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