diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf5f9ab6c3..f902f6c0c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * `ERC165`: Remove uses of storage in the base ERC165 implementation. ERC165 based contracts now use storage-less virtual functions. Old behaviour remains available in the `ERC165Storage` extension. ([#2505](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2505)) * `Initializable`: Make initializer check stricter during construction. ([#2531](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2531)) * `ERC721`: remove enumerability of tokens from the base implementation. This feature is now provided separately through the `ERC721Enumerable` extension. ([#2511](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2511)) + * `AccessControl`: removed enumerability by default for a more lightweight contract. It is now opt-in through `AccessControlEnumerable`. ([#2512](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2512)) ## 3.4.0 (2021-02-02) diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 1871e33a70b..708ec9adac2 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -2,13 +2,14 @@ pragma solidity ^0.8.0; -import "../utils/EnumerableSet.sol"; -import "../utils/Address.sol"; import "../utils/Context.sol"; /** * @dev Contract module that allows children to implement role-based access - * control mechanisms. + * control mechanisms. This is a lightweight version that doesn't allow enumerating role + * members except through off-chain means by accessing the contract event logs. Some + * applications may benefit from on-chain enumerability, for those cases see + * {AccessControlEnumerable}. * * Roles are referred to by their `bytes32` identifier. These should be exposed * in the external API and be unique. The best way to achieve this is by @@ -42,11 +43,8 @@ import "../utils/Context.sol"; * accounts that have been granted it. */ abstract contract AccessControl is Context { - using EnumerableSet for EnumerableSet.AddressSet; - using Address for address; - struct RoleData { - EnumerableSet.AddressSet members; + mapping (address => bool) members; bytes32 adminRole; } @@ -85,31 +83,7 @@ abstract contract AccessControl is Context { * @dev Returns `true` if `account` has been granted `role`. */ function hasRole(bytes32 role, address account) public view returns (bool) { - return _roles[role].members.contains(account); - } - - /** - * @dev Returns the number of accounts that have `role`. Can be used - * together with {getRoleMember} to enumerate all bearers of a role. - */ - function getRoleMemberCount(bytes32 role) public view returns (uint256) { - return _roles[role].members.length(); - } - - /** - * @dev Returns one of the accounts that have `role`. `index` must be a - * value between 0 and {getRoleMemberCount}, non-inclusive. - * - * Role bearers are not sorted in any particular way, and their ordering may - * change at any point. - * - * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure - * you perform all queries on the same block. See the following - * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] - * for more information. - */ - function getRoleMember(bytes32 role, uint256 index) public view returns (address) { - return _roles[role].members.at(index); + return _roles[role].members[account]; } /** @@ -133,7 +107,7 @@ abstract contract AccessControl is Context { * - the caller must have ``role``'s admin role. */ function grantRole(bytes32 role, address account) public virtual { - require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to grant"); + require(hasRole(getRoleAdmin(role), _msgSender()), "AccessControl: sender must be an admin to grant"); _grantRole(role, account); } @@ -148,7 +122,7 @@ abstract contract AccessControl is Context { * - the caller must have ``role``'s admin role. */ function revokeRole(bytes32 role, address account) public virtual { - require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to revoke"); + require(hasRole(getRoleAdmin(role), _msgSender()), "AccessControl: sender must be an admin to revoke"); _revokeRole(role, account); } @@ -199,18 +173,20 @@ abstract contract AccessControl is Context { * Emits a {RoleAdminChanged} event. */ function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { - emit RoleAdminChanged(role, _roles[role].adminRole, adminRole); + emit RoleAdminChanged(role, getRoleAdmin(role), adminRole); _roles[role].adminRole = adminRole; } function _grantRole(bytes32 role, address account) private { - if (_roles[role].members.add(account)) { + if (!hasRole(role, account)) { + _roles[role].members[account] = true; emit RoleGranted(role, account, _msgSender()); } } function _revokeRole(bytes32 role, address account) private { - if (_roles[role].members.remove(account)) { + if (hasRole(role, account)) { + _roles[role].members[account] = false; emit RoleRevoked(role, account, _msgSender()); } } diff --git a/contracts/access/AccessControlEnumerable.sol b/contracts/access/AccessControlEnumerable.sol new file mode 100644 index 00000000000..be58a7b8de8 --- /dev/null +++ b/contracts/access/AccessControlEnumerable.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./AccessControl.sol"; +import "../utils/EnumerableSet.sol"; + +/** + * @dev Extension of {AccessControl} that allows enumerating the members of each role. + */ +abstract contract AccessControlEnumerable is AccessControl { + using EnumerableSet for EnumerableSet.AddressSet; + + mapping (bytes32 => EnumerableSet.AddressSet) private _roleMembers; + + /** + * @dev Returns one of the accounts that have `role`. `index` must be a + * value between 0 and {getRoleMemberCount}, non-inclusive. + * + * Role bearers are not sorted in any particular way, and their ordering may + * change at any point. + * + * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure + * you perform all queries on the same block. See the following + * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] + * for more information. + */ + function getRoleMember(bytes32 role, uint256 index) public view returns (address) { + return _roleMembers[role].at(index); + } + + /** + * @dev Returns the number of accounts that have `role`. Can be used + * together with {getRoleMember} to enumerate all bearers of a role. + */ + function getRoleMemberCount(bytes32 role) public view returns (uint256) { + return _roleMembers[role].length(); + } + + /** + * @dev Overload {grantRole} to track enumerable memberships + */ + function grantRole(bytes32 role, address account) public virtual override { + super.grantRole(role, account); + _roleMembers[role].add(account); + } + + /** + * @dev Overload {revokeRole} to track enumerable memberships + */ + function revokeRole(bytes32 role, address account) public virtual override { + super.revokeRole(role, account); + _roleMembers[role].remove(account); + } + + /** + * @dev Overload {_setupRole} to track enumerable memberships + */ + function _setupRole(bytes32 role, address account) internal virtual override { + super._setupRole(role, account); + _roleMembers[role].add(account); + } +} diff --git a/contracts/access/README.adoc b/contracts/access/README.adoc index 67496c57e2a..c1431c1200e 100644 --- a/contracts/access/README.adoc +++ b/contracts/access/README.adoc @@ -15,6 +15,8 @@ This directory provides ways to restrict who can access the functions of a contr {{AccessControl}} +{{AccessControlEnumerable}} + == Timelock {{TimelockController}} diff --git a/contracts/mocks/AccessControlEnumerableMock.sol b/contracts/mocks/AccessControlEnumerableMock.sol new file mode 100644 index 00000000000..ec2cbbb103f --- /dev/null +++ b/contracts/mocks/AccessControlEnumerableMock.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../access/AccessControlEnumerable.sol"; + +contract AccessControlEnumerableMock is AccessControlEnumerable { + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + function setRoleAdmin(bytes32 roleId, bytes32 adminRoleId) public { + _setRoleAdmin(roleId, adminRoleId); + } +} diff --git a/contracts/presets/ERC1155PresetMinterPauser.sol b/contracts/presets/ERC1155PresetMinterPauser.sol index 73fabafdd19..e8423f8b4ca 100644 --- a/contracts/presets/ERC1155PresetMinterPauser.sol +++ b/contracts/presets/ERC1155PresetMinterPauser.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import "../access/AccessControl.sol"; +import "../access/AccessControlEnumerable.sol"; import "../utils/Context.sol"; import "../token/ERC1155/ERC1155.sol"; import "../token/ERC1155/ERC1155Burnable.sol"; @@ -22,7 +22,7 @@ import "../token/ERC1155/ERC1155Pausable.sol"; * roles, as well as the default admin role, which will let it grant both minter * and pauser roles to other accounts. */ -contract ERC1155PresetMinterPauser is Context, AccessControl, ERC1155Burnable, ERC1155Pausable { +contract ERC1155PresetMinterPauser is Context, AccessControlEnumerable, ERC1155Burnable, ERC1155Pausable { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); diff --git a/contracts/presets/ERC20PresetMinterPauser.sol b/contracts/presets/ERC20PresetMinterPauser.sol index a8891c4426c..a9047d996fd 100644 --- a/contracts/presets/ERC20PresetMinterPauser.sol +++ b/contracts/presets/ERC20PresetMinterPauser.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import "../access/AccessControl.sol"; +import "../access/AccessControlEnumerable.sol"; import "../utils/Context.sol"; import "../token/ERC20/ERC20.sol"; import "../token/ERC20/ERC20Burnable.sol"; @@ -22,7 +22,7 @@ import "../token/ERC20/ERC20Pausable.sol"; * roles, as well as the default admin role, which will let it grant both minter * and pauser roles to other accounts. */ -contract ERC20PresetMinterPauser is Context, AccessControl, ERC20Burnable, ERC20Pausable { +contract ERC20PresetMinterPauser is Context, AccessControlEnumerable, ERC20Burnable, ERC20Pausable { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); diff --git a/contracts/presets/ERC721PresetMinterPauserAutoId.sol b/contracts/presets/ERC721PresetMinterPauserAutoId.sol index 7117dee42e6..ca1b6b95aee 100644 --- a/contracts/presets/ERC721PresetMinterPauserAutoId.sol +++ b/contracts/presets/ERC721PresetMinterPauserAutoId.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import "../access/AccessControl.sol"; +import "../access/AccessControlEnumerable.sol"; import "../utils/Context.sol"; import "../utils/Counters.sol"; import "../token/ERC721/ERC721.sol"; @@ -25,7 +25,7 @@ import "../token/ERC721/ERC721Pausable.sol"; * roles, as well as the default admin role, which will let it grant both minter * and pauser roles to other accounts. */ -contract ERC721PresetMinterPauserAutoId is Context, AccessControl, ERC721Enumerable, ERC721Burnable, ERC721Pausable { +contract ERC721PresetMinterPauserAutoId is Context, AccessControlEnumerable, ERC721Enumerable, ERC721Burnable, ERC721Pausable { using Counters for Counters.Counter; bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js new file mode 100644 index 00000000000..5b7c32fa3c2 --- /dev/null +++ b/test/access/AccessControl.behavior.js @@ -0,0 +1,182 @@ +const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); + +const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; +const ROLE = web3.utils.soliditySha3('ROLE'); +const OTHER_ROLE = web3.utils.soliditySha3('OTHER_ROLE'); + +function shouldBehaveLikeAccessControl (errorPrefix, admin, authorized, other, otherAdmin, otherAuthorized) { + describe('default admin', function () { + it('deployer has default admin role', async function () { + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, admin)).to.equal(true); + }); + + it('other roles\'s admin is the default admin role', async function () { + expect(await this.accessControl.getRoleAdmin(ROLE)).to.equal(DEFAULT_ADMIN_ROLE); + }); + + it('default admin role\'s admin is itself', async function () { + expect(await this.accessControl.getRoleAdmin(DEFAULT_ADMIN_ROLE)).to.equal(DEFAULT_ADMIN_ROLE); + }); + }); + + describe('granting', function () { + it('admin can grant role to other accounts', async function () { + const receipt = await this.accessControl.grantRole(ROLE, authorized, { from: admin }); + expectEvent(receipt, 'RoleGranted', { account: authorized, role: ROLE, sender: admin }); + + expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(true); + }); + + it('non-admin cannot grant role to other accounts', async function () { + await expectRevert( + this.accessControl.grantRole(ROLE, authorized, { from: other }), + `${errorPrefix}: sender must be an admin to grant`, + ); + }); + + it('accounts can be granted a role multiple times', async function () { + await this.accessControl.grantRole(ROLE, authorized, { from: admin }); + const receipt = await this.accessControl.grantRole(ROLE, authorized, { from: admin }); + expectEvent.notEmitted(receipt, 'RoleGranted'); + }); + }); + + describe('revoking', function () { + it('roles that are not had can be revoked', async function () { + expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false); + + const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin }); + expectEvent.notEmitted(receipt, 'RoleRevoked'); + }); + + context('with granted role', function () { + beforeEach(async function () { + await this.accessControl.grantRole(ROLE, authorized, { from: admin }); + }); + + it('admin can revoke role', async function () { + const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin }); + expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: admin }); + + expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false); + }); + + it('non-admin cannot revoke role', async function () { + await expectRevert( + this.accessControl.revokeRole(ROLE, authorized, { from: other }), + `${errorPrefix}: sender must be an admin to revoke`, + ); + }); + + it('a role can be revoked multiple times', async function () { + await this.accessControl.revokeRole(ROLE, authorized, { from: admin }); + + const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin }); + expectEvent.notEmitted(receipt, 'RoleRevoked'); + }); + }); + }); + + describe('renouncing', function () { + it('roles that are not had can be renounced', async function () { + const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized }); + expectEvent.notEmitted(receipt, 'RoleRevoked'); + }); + + context('with granted role', function () { + beforeEach(async function () { + await this.accessControl.grantRole(ROLE, authorized, { from: admin }); + }); + + it('bearer can renounce role', async function () { + const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized }); + expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: authorized }); + + expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false); + }); + + it('only the sender can renounce their roles', async function () { + await expectRevert( + this.accessControl.renounceRole(ROLE, authorized, { from: admin }), + `${errorPrefix}: can only renounce roles for self`, + ); + }); + + it('a role can be renounced multiple times', async function () { + await this.accessControl.renounceRole(ROLE, authorized, { from: authorized }); + + const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized }); + expectEvent.notEmitted(receipt, 'RoleRevoked'); + }); + }); + }); + + describe('setting role admin', function () { + beforeEach(async function () { + const receipt = await this.accessControl.setRoleAdmin(ROLE, OTHER_ROLE); + expectEvent(receipt, 'RoleAdminChanged', { + role: ROLE, + previousAdminRole: DEFAULT_ADMIN_ROLE, + newAdminRole: OTHER_ROLE, + }); + + await this.accessControl.grantRole(OTHER_ROLE, otherAdmin, { from: admin }); + }); + + it('a role\'s admin role can be changed', async function () { + expect(await this.accessControl.getRoleAdmin(ROLE)).to.equal(OTHER_ROLE); + }); + + it('the new admin can grant roles', async function () { + const receipt = await this.accessControl.grantRole(ROLE, authorized, { from: otherAdmin }); + expectEvent(receipt, 'RoleGranted', { account: authorized, role: ROLE, sender: otherAdmin }); + }); + + it('the new admin can revoke roles', async function () { + await this.accessControl.grantRole(ROLE, authorized, { from: otherAdmin }); + const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: otherAdmin }); + expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: otherAdmin }); + }); + + it('a role\'s previous admins no longer grant roles', async function () { + await expectRevert( + this.accessControl.grantRole(ROLE, authorized, { from: admin }), + 'AccessControl: sender must be an admin to grant', + ); + }); + + it('a role\'s previous admins no longer revoke roles', async function () { + await expectRevert( + this.accessControl.revokeRole(ROLE, authorized, { from: admin }), + 'AccessControl: sender must be an admin to revoke', + ); + }); + }); +} + +function shouldBehaveLikeAccessControlEnumerable (errorPrefix, admin, authorized, other, otherAdmin, otherAuthorized) { + describe('enumerating', function () { + it('role bearers can be enumerated', async function () { + await this.accessControl.grantRole(ROLE, authorized, { from: admin }); + await this.accessControl.grantRole(ROLE, other, { from: admin }); + await this.accessControl.grantRole(ROLE, otherAuthorized, { from: admin }); + await this.accessControl.revokeRole(ROLE, other, { from: admin }); + + const memberCount = await this.accessControl.getRoleMemberCount(ROLE); + expect(memberCount).to.bignumber.equal('2'); + + const bearers = []; + for (let i = 0; i < memberCount; ++i) { + bearers.push(await this.accessControl.getRoleMember(ROLE, i)); + } + + expect(bearers).to.have.members([authorized, otherAuthorized]); + }); + }); +} + +module.exports = { + shouldBehaveLikeAccessControl, + shouldBehaveLikeAccessControlEnumerable, +}; diff --git a/test/access/AccessControl.test.js b/test/access/AccessControl.test.js index f86bce573d5..cd9912adb4f 100644 --- a/test/access/AccessControl.test.js +++ b/test/access/AccessControl.test.js @@ -1,182 +1,13 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); +const { + shouldBehaveLikeAccessControl, +} = require('./AccessControl.behavior.js'); const AccessControlMock = artifacts.require('AccessControlMock'); contract('AccessControl', function (accounts) { - const [ admin, authorized, otherAuthorized, other, otherAdmin ] = accounts; - - const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const ROLE = web3.utils.soliditySha3('ROLE'); - const OTHER_ROLE = web3.utils.soliditySha3('OTHER_ROLE'); - beforeEach(async function () { - this.accessControl = await AccessControlMock.new({ from: admin }); - }); - - describe('default admin', function () { - it('deployer has default admin role', async function () { - expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, admin)).to.equal(true); - }); - - it('other roles\'s admin is the default admin role', async function () { - expect(await this.accessControl.getRoleAdmin(ROLE)).to.equal(DEFAULT_ADMIN_ROLE); - }); - - it('default admin role\'s admin is itself', async function () { - expect(await this.accessControl.getRoleAdmin(DEFAULT_ADMIN_ROLE)).to.equal(DEFAULT_ADMIN_ROLE); - }); - }); - - describe('granting', function () { - it('admin can grant role to other accounts', async function () { - const receipt = await this.accessControl.grantRole(ROLE, authorized, { from: admin }); - expectEvent(receipt, 'RoleGranted', { account: authorized, role: ROLE, sender: admin }); - - expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(true); - }); - - it('non-admin cannot grant role to other accounts', async function () { - await expectRevert( - this.accessControl.grantRole(ROLE, authorized, { from: other }), - 'AccessControl: sender must be an admin to grant', - ); - }); - - it('accounts can be granted a role multiple times', async function () { - await this.accessControl.grantRole(ROLE, authorized, { from: admin }); - const receipt = await this.accessControl.grantRole(ROLE, authorized, { from: admin }); - expectEvent.notEmitted(receipt, 'RoleGranted'); - }); - }); - - describe('revoking', function () { - it('roles that are not had can be revoked', async function () { - expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false); - - const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin }); - expectEvent.notEmitted(receipt, 'RoleRevoked'); - }); - - context('with granted role', function () { - beforeEach(async function () { - await this.accessControl.grantRole(ROLE, authorized, { from: admin }); - }); - - it('admin can revoke role', async function () { - const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin }); - expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: admin }); - - expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false); - }); - - it('non-admin cannot revoke role', async function () { - await expectRevert( - this.accessControl.revokeRole(ROLE, authorized, { from: other }), - 'AccessControl: sender must be an admin to revoke', - ); - }); - - it('a role can be revoked multiple times', async function () { - await this.accessControl.revokeRole(ROLE, authorized, { from: admin }); - - const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: admin }); - expectEvent.notEmitted(receipt, 'RoleRevoked'); - }); - }); + this.accessControl = await AccessControlMock.new({ from: accounts[0] }); }); - describe('renouncing', function () { - it('roles that are not had can be renounced', async function () { - const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized }); - expectEvent.notEmitted(receipt, 'RoleRevoked'); - }); - - context('with granted role', function () { - beforeEach(async function () { - await this.accessControl.grantRole(ROLE, authorized, { from: admin }); - }); - - it('bearer can renounce role', async function () { - const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized }); - expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: authorized }); - - expect(await this.accessControl.hasRole(ROLE, authorized)).to.equal(false); - }); - - it('only the sender can renounce their roles', async function () { - await expectRevert( - this.accessControl.renounceRole(ROLE, authorized, { from: admin }), - 'AccessControl: can only renounce roles for self', - ); - }); - - it('a role can be renounced multiple times', async function () { - await this.accessControl.renounceRole(ROLE, authorized, { from: authorized }); - - const receipt = await this.accessControl.renounceRole(ROLE, authorized, { from: authorized }); - expectEvent.notEmitted(receipt, 'RoleRevoked'); - }); - }); - }); - - describe('enumerating', function () { - it('role bearers can be enumerated', async function () { - await this.accessControl.grantRole(ROLE, authorized, { from: admin }); - await this.accessControl.grantRole(ROLE, otherAuthorized, { from: admin }); - - const memberCount = await this.accessControl.getRoleMemberCount(ROLE); - expect(memberCount).to.bignumber.equal('2'); - - const bearers = []; - for (let i = 0; i < memberCount; ++i) { - bearers.push(await this.accessControl.getRoleMember(ROLE, i)); - } - - expect(bearers).to.have.members([authorized, otherAuthorized]); - }); - }); - - describe('setting role admin', function () { - beforeEach(async function () { - const receipt = await this.accessControl.setRoleAdmin(ROLE, OTHER_ROLE); - expectEvent(receipt, 'RoleAdminChanged', { - role: ROLE, - previousAdminRole: DEFAULT_ADMIN_ROLE, - newAdminRole: OTHER_ROLE, - }); - - await this.accessControl.grantRole(OTHER_ROLE, otherAdmin, { from: admin }); - }); - - it('a role\'s admin role can be changed', async function () { - expect(await this.accessControl.getRoleAdmin(ROLE)).to.equal(OTHER_ROLE); - }); - - it('the new admin can grant roles', async function () { - const receipt = await this.accessControl.grantRole(ROLE, authorized, { from: otherAdmin }); - expectEvent(receipt, 'RoleGranted', { account: authorized, role: ROLE, sender: otherAdmin }); - }); - - it('the new admin can revoke roles', async function () { - await this.accessControl.grantRole(ROLE, authorized, { from: otherAdmin }); - const receipt = await this.accessControl.revokeRole(ROLE, authorized, { from: otherAdmin }); - expectEvent(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: otherAdmin }); - }); - - it('a role\'s previous admins no longer grant roles', async function () { - await expectRevert( - this.accessControl.grantRole(ROLE, authorized, { from: admin }), - 'AccessControl: sender must be an admin to grant', - ); - }); - - it('a role\'s previous admins no longer revoke roles', async function () { - await expectRevert( - this.accessControl.revokeRole(ROLE, authorized, { from: admin }), - 'AccessControl: sender must be an admin to revoke', - ); - }); - }); + shouldBehaveLikeAccessControl('AccessControl', ...accounts); }); diff --git a/test/access/AccessControlEnumerable.test.js b/test/access/AccessControlEnumerable.test.js new file mode 100644 index 00000000000..fa5b54691da --- /dev/null +++ b/test/access/AccessControlEnumerable.test.js @@ -0,0 +1,15 @@ +const { + shouldBehaveLikeAccessControl, + shouldBehaveLikeAccessControlEnumerable, +} = require('./AccessControl.behavior.js'); + +const AccessControlMock = artifacts.require('AccessControlEnumerableMock'); + +contract('AccessControl', function (accounts) { + beforeEach(async function () { + this.accessControl = await AccessControlMock.new({ from: accounts[0] }); + }); + + shouldBehaveLikeAccessControl('AccessControl', ...accounts); + shouldBehaveLikeAccessControlEnumerable('AccessControl', ...accounts); +});