diff --git a/.changeset/violet-moons-tell.md b/.changeset/violet-moons-tell.md new file mode 100644 index 000000000..be215e193 --- /dev/null +++ b/.changeset/violet-moons-tell.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`AccessControlEnumerable`: Add a `getRoleMembers` method to return all accounts that have `role`. diff --git a/contracts/access/extensions/AccessControlEnumerable.sol b/contracts/access/extensions/AccessControlEnumerable.sol index 151de05c4..222490c9b 100644 --- a/contracts/access/extensions/AccessControlEnumerable.sol +++ b/contracts/access/extensions/AccessControlEnumerable.sol @@ -46,6 +46,18 @@ abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessCon return _roleMembers[role].length(); } + /** + * @dev Return all accounts that have `role` + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function getRoleMembers(bytes32 role) public view virtual returns (address[] memory) { + return _roleMembers[role].values(); + } + /** * @dev Overload {AccessControl-_grantRole} to track enumerable memberships */ diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index 1b4e27a61..56290c0c1 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -239,15 +239,17 @@ function shouldBehaveLikeAccessControlEnumerable() { await this.mock.connect(this.defaultAdmin).grantRole(ROLE, this.otherAuthorized); await this.mock.connect(this.defaultAdmin).revokeRole(ROLE, this.other); - const memberCount = await this.mock.getRoleMemberCount(ROLE); - expect(memberCount).to.equal(2); + const expectedMembers = [this.authorized.address, this.otherAuthorized.address]; - const bearers = []; + const memberCount = await this.mock.getRoleMemberCount(ROLE); + const members = []; for (let i = 0; i < memberCount; ++i) { - bearers.push(await this.mock.getRoleMember(ROLE, i)); + members.push(await this.mock.getRoleMember(ROLE, i)); } - expect(bearers).to.have.members([this.authorized.address, this.otherAuthorized.address]); + expect(memberCount).to.equal(expectedMembers.length); + expect(members).to.deep.equal(expectedMembers); + expect(await this.mock.getRoleMembers(ROLE)).to.deep.equal(expectedMembers); }); it('role enumeration should be in sync after renounceRole call', async function () {