From 4374b53cb884d8c54c5a512d79eb64d91bd59d7d Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 4 Nov 2025 11:35:29 -0600 Subject: [PATCH 1/8] Add AccessManagerEnumerable --- .../extensions/AccessManagerEnumerable.sol | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 contracts/access/manager/extensions/AccessManagerEnumerable.sol diff --git a/contracts/access/manager/extensions/AccessManagerEnumerable.sol b/contracts/access/manager/extensions/AccessManagerEnumerable.sol new file mode 100644 index 00000000000..4728bf56eb0 --- /dev/null +++ b/contracts/access/manager/extensions/AccessManagerEnumerable.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {AccessManager} from "../AccessManager.sol"; +import {EnumerableSet} from "../../../utils/structs/EnumerableSet.sol"; + +abstract contract AccessManagerEnumerable is AccessManager { + using EnumerableSet for EnumerableSet.AddressSet; + mapping(uint64 roleId => EnumerableSet.AddressSet) private _roleMembers; + + function _grantRole( + uint64 roleId, + address account, + uint32 grantDelay, + uint32 executionDelay + ) internal override returns (bool) { + bool granted = super._grantRole(roleId, account, grantDelay, executionDelay); + if (granted) { + _roleMembers[roleId].add(account); + } + return granted; + } + + function _revokeRole(uint64 roleId, address account) internal override returns (bool) { + bool revoked = super._revokeRole(roleId, account); + if (revoked) { + _roleMembers[roleId].remove(account); + } + return revoked; + } + + function getRoleMembers(uint64 roleId, uint256 start, uint256 end) public view returns (address[] memory) { + return _roleMembers[roleId].values(start, end); + } + + function getRoleMemberCount(uint64 roleId) public view returns (uint256) { + return _roleMembers[roleId].length(); + } +} From 841e6bb90547ca031fbcc04012fbae7329365931 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 10 Nov 2025 11:56:27 -0300 Subject: [PATCH 2/8] Add interface, docs and tests --- .../extensions/AccessManagerEnumerable.sol | 105 ++++++++++- .../extensions/IAccessManagerEnumerable.sol | 78 +++++++++ contracts/mocks/AccessManagerMock.sol | 29 ++++ test/access/manager/AccessManager.behavior.js | 164 ++++++++++++++++++ .../AccessManagerEnumerable.test.js | 62 +++++++ 5 files changed, 429 insertions(+), 9 deletions(-) create mode 100644 contracts/access/manager/extensions/IAccessManagerEnumerable.sol create mode 100644 test/access/manager/extensions/AccessManagerEnumerable.test.js diff --git a/contracts/access/manager/extensions/AccessManagerEnumerable.sol b/contracts/access/manager/extensions/AccessManagerEnumerable.sol index 4728bf56eb0..256da227c48 100644 --- a/contracts/access/manager/extensions/AccessManagerEnumerable.sol +++ b/contracts/access/manager/extensions/AccessManagerEnumerable.sol @@ -2,19 +2,81 @@ pragma solidity ^0.8.20; +import {IAccessManagerEnumerable} from "./IAccessManagerEnumerable.sol"; import {AccessManager} from "../AccessManager.sol"; import {EnumerableSet} from "../../../utils/structs/EnumerableSet.sol"; -abstract contract AccessManagerEnumerable is AccessManager { +/** + * @dev Extension of {AccessManager} that allows enumerating the members of each role + * and the target functions each role is allowed to call. + * + * NOTE: Given {ADMIN_ROLE} is the default role for every restricted function, the + * {getRoleTargetFunctions} and {getRoleTargetFunctionCount} functions will return an empty array + * and 0 respectively. + */ +abstract contract AccessManagerEnumerable is IAccessManagerEnumerable, AccessManager { using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.Bytes32Set; + mapping(uint64 roleId => EnumerableSet.AddressSet) private _roleMembers; + mapping(uint64 roleId => EnumerableSet.Bytes32Set) private _roleTargetFunctions; + + /// @inheritdoc IAccessManagerEnumerable + function getRoleMember(uint64 roleId, uint256 index) public view virtual returns (address) { + return _roleMembers[roleId].at(index); + } + + /// @inheritdoc IAccessManagerEnumerable + function getRoleMembers(uint64 roleId, uint256 start, uint256 end) public view virtual returns (address[] memory) { + return _roleMembers[roleId].values(start, end); + } + /// @inheritdoc IAccessManagerEnumerable + function getRoleMemberCount(uint64 roleId) public view virtual returns (uint256) { + return _roleMembers[roleId].length(); + } + + /// @inheritdoc IAccessManagerEnumerable + function getRoleTargetFunction(uint64 roleId, uint256 index) public view virtual returns (bytes4) { + return bytes4(_roleTargetFunctions[roleId].at(index)); + } + + /* + * @dev See {IAccessManagerEnumerable-getRoleTargetFunctions} + * + * NOTE: Given {ADMIN_ROLE} is the default role for every restricted function, passing {ADMIN_ROLE} as `roleId` will + * return an empty array. See {_setTargetFunctionRole} for more details. + */ + function getRoleTargetFunctions( + uint64 roleId, + uint256 start, + uint256 end + ) public view virtual returns (bytes4[] memory) { + bytes32[] memory targetFunctions = _roleTargetFunctions[roleId].values(start, end); + bytes4[] memory targetFunctionSelectors; + assembly ("memory-safe") { + targetFunctionSelectors := targetFunctions + } + return targetFunctionSelectors; + } + + /* + * @dev See {IAccessManagerEnumerable-getRoleTargetFunctionCount} + * + * NOTE: Given {ADMIN_ROLE} is the default role for every restricted function, passing {ADMIN_ROLE} as `roleId` will + * return 0. See {_setTargetFunctionRole} for more details. + */ + function getRoleTargetFunctionCount(uint64 roleId) public view virtual returns (uint256) { + return _roleTargetFunctions[roleId].length(); + } + + /// @dev See {AccessManager-_grantRole}. Adds the account to the role members set. function _grantRole( uint64 roleId, address account, uint32 grantDelay, uint32 executionDelay - ) internal override returns (bool) { + ) internal virtual override returns (bool) { bool granted = super._grantRole(roleId, account, grantDelay, executionDelay); if (granted) { _roleMembers[roleId].add(account); @@ -22,7 +84,8 @@ abstract contract AccessManagerEnumerable is AccessManager { return granted; } - function _revokeRole(uint64 roleId, address account) internal override returns (bool) { + /// @dev See {AccessManager-_revokeRole}. Removes the account from the role members set. + function _revokeRole(uint64 roleId, address account) internal virtual override returns (bool) { bool revoked = super._revokeRole(roleId, account); if (revoked) { _roleMembers[roleId].remove(account); @@ -30,11 +93,35 @@ abstract contract AccessManagerEnumerable is AccessManager { return revoked; } - function getRoleMembers(uint64 roleId, uint256 start, uint256 end) public view returns (address[] memory) { - return _roleMembers[roleId].values(start, end); - } - - function getRoleMemberCount(uint64 roleId) public view returns (uint256) { - return _roleMembers[roleId].length(); + /** + * @dev See {AccessManager-_setTargetFunctionRole}. Adds the selector to the role target functions set. + * + * Since the target functions for the {ADMIN_ROLE} can't be tracked exhaustively (i.e. by default, all + * restricted functions), any function that is granted to the {ADMIN_ROLE} will not be tracked by this + * extension. Developers may opt in for tracking the functions for the {ADMIN_ROLE} by overriding, + * though, the tracking would not be exhaustive. + * + * ```solidity + * function _setTargetFunctionRole(address target, bytes4 selector, uint64 roleId) internal virtual override { + * uint64 oldRoleId = getTargetFunctionRole(target, selector); + * super._setTargetFunctionRole(target, selector, roleId); + * if (oldRoleId == ADMIN_ROLE) { + * _roleTargetFunctions[oldRoleId].remove(bytes32(selector)); + * } + * if (roleId == ADMIN_ROLE) { + * _roleTargetFunctions[roleId].add(bytes32(selector)); + * } + * } + * ``` + */ + function _setTargetFunctionRole(address target, bytes4 selector, uint64 roleId) internal virtual override { + uint64 oldRoleId = getTargetFunctionRole(target, selector); + super._setTargetFunctionRole(target, selector, roleId); + if (oldRoleId != ADMIN_ROLE) { + _roleTargetFunctions[oldRoleId].remove(bytes32(selector)); + } + if (roleId != ADMIN_ROLE) { + _roleTargetFunctions[roleId].add(bytes32(selector)); + } } } diff --git a/contracts/access/manager/extensions/IAccessManagerEnumerable.sol b/contracts/access/manager/extensions/IAccessManagerEnumerable.sol new file mode 100644 index 00000000000..6d94d41d1b1 --- /dev/null +++ b/contracts/access/manager/extensions/IAccessManagerEnumerable.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.4; + +import {IAccessManager} from "../IAccessManager.sol"; + +/** + * @dev External interface of AccessManagerEnumerable. + */ +interface IAccessManagerEnumerable is IAccessManager { + /** + * @dev Returns one of the accounts that have `roleId`. `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(uint64 roleId, uint256 index) external view returns (address); + + /** + * @dev Returns a range of accounts that have `roleId`. `start` and `end` define the range bounds. + * `start` is inclusive and `end` is exclusive. + * + * Role bearers are not sorted in any particular way, and their ordering may + * change at any point. + * + * 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(uint64 roleId, uint256 start, uint256 end) external view returns (address[] memory); + + /** + * @dev Returns the number of accounts that have `roleId`. Can be used + * together with {getRoleMember} to enumerate all bearers of a role. + */ + function getRoleMemberCount(uint64 roleId) external view returns (uint256); + + /** + * @dev Returns one of the target function selectors that require `roleId`. `index` must be a + * value between 0 and {getRoleTargetFunctionCount}, non-inclusive. + * + * Target function selectors are not sorted in any particular way, and their ordering may + * change at any point. + * + * WARNING: When using {getRoleTargetFunction} and {getRoleTargetFunctionCount}, 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 getRoleTargetFunction(uint64 roleId, uint256 index) external view returns (bytes4); + + /** + * @dev Returns a range of target function selectors that require `roleId`. `start` and `end` define the range bounds. + * `start` is inclusive and `end` is exclusive. + * + * Target function selectors are not sorted in any particular way, and their ordering may + * change at any point. + * + * 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 getRoleTargetFunctions(uint64 roleId, uint256 start, uint256 end) external view returns (bytes4[] memory); + + /** + * @dev Returns the number of target function selectors that require `roleId`. Can be used + * together with {getRoleTargetFunction} to enumerate all target functions for a role. + */ + function getRoleTargetFunctionCount(uint64 roleId) external view returns (uint256); +} diff --git a/contracts/mocks/AccessManagerMock.sol b/contracts/mocks/AccessManagerMock.sol index 4b5be350fc6..4d0623142bb 100644 --- a/contracts/mocks/AccessManagerMock.sol +++ b/contracts/mocks/AccessManagerMock.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {AccessManager} from "../access/manager/AccessManager.sol"; +import {AccessManagerEnumerable} from "../access/manager/extensions/AccessManagerEnumerable.sol"; contract AccessManagerMock is AccessManager { event CalledRestricted(address caller); @@ -18,3 +19,31 @@ contract AccessManagerMock is AccessManager { emit CalledUnrestricted(msg.sender); } } + +contract AccessManagerEnumerableMock is AccessManagerMock, AccessManagerEnumerable { + constructor(address initialAdmin) AccessManagerMock(initialAdmin) {} + + function _grantRole( + uint64 roleId, + address account, + uint32 grantDelay, + uint32 executionDelay + ) internal override(AccessManager, AccessManagerEnumerable) returns (bool) { + return super._grantRole(roleId, account, grantDelay, executionDelay); + } + + function _revokeRole( + uint64 roleId, + address account + ) internal override(AccessManager, AccessManagerEnumerable) returns (bool) { + return super._revokeRole(roleId, account); + } + + function _setTargetFunctionRole( + address target, + bytes4 selector, + uint64 roleId + ) internal override(AccessManager, AccessManagerEnumerable) { + super._setTargetFunctionRole(target, selector, roleId); + } +} diff --git a/test/access/manager/AccessManager.behavior.js b/test/access/manager/AccessManager.behavior.js index 830700e3762..5d63ca2a7a7 100644 --- a/test/access/manager/AccessManager.behavior.js +++ b/test/access/manager/AccessManager.behavior.js @@ -1,5 +1,6 @@ const { expect } = require('chai'); +const { selector } = require('../../helpers/methods'); const { LIKE_COMMON_IS_EXECUTING, LIKE_COMMON_GET_ACCESS, @@ -248,10 +249,173 @@ function shouldBehaveLikeASelfRestrictedOperation() { }); } +// ============ ENUMERABLE EXTENSION ============ + +/** + * @requires this.{manager,roles,admin,user,other} + */ +function shouldBehaveLikeAccessManagerEnumerable() { + describe('enumerating', function () { + const ANOTHER_ROLE = 0xdeadc0de2n; + + describe('role members', function () { + it('role bearers can be enumerated', async function () { + // Grant roles to multiple accounts + await this.manager.connect(this.admin).grantRole(ANOTHER_ROLE, this.user, 0); + await this.manager.connect(this.admin).grantRole(ANOTHER_ROLE, this.other, 0); + await this.manager.connect(this.admin).grantRole(ANOTHER_ROLE, this.admin, 0); + + // Revoke one role + await this.manager.connect(this.admin).revokeRole(ANOTHER_ROLE, this.other); + + const expectedMembers = [this.user.address, this.admin.address]; + + // Test individual enumeration + const memberCount = await this.manager.getRoleMemberCount(ANOTHER_ROLE); + const members = []; + for (let i = 0; i < memberCount; ++i) { + members.push(await this.manager.getRoleMember(ANOTHER_ROLE, i)); + } + + expect(memberCount).to.equal(expectedMembers.length); + expect(members).to.deep.equal(expectedMembers); + + // Test batch enumeration + await expect(this.manager.getRoleMembers(ANOTHER_ROLE, 0, memberCount)).to.eventually.deep.equal( + expectedMembers, + ); + }); + + it('role enumeration should be in sync after renounceRole call', async function () { + await this.manager.connect(this.admin).grantRole(ANOTHER_ROLE, this.user, 0); + + await expect(this.manager.getRoleMemberCount(ANOTHER_ROLE)).to.eventually.equal(1); // Only the initial member + await this.manager.connect(this.admin).grantRole(ANOTHER_ROLE, this.admin, 0); + await expect(this.manager.getRoleMemberCount(ANOTHER_ROLE)).to.eventually.equal(2); + await this.manager.connect(this.admin).renounceRole(ANOTHER_ROLE, this.admin); + await expect(this.manager.getRoleMemberCount(ANOTHER_ROLE)).to.eventually.equal(1); + }); + + it('returns empty for roles with no members', async function () { + const roleId = 999n; // Non-existent role + + await expect(this.manager.getRoleMemberCount(roleId)).to.eventually.equal(0); + await expect(this.manager.getRoleMembers(roleId, 0, 10)).to.eventually.deep.equal([]); + }); + + it('supports partial enumeration with start and end parameters', async function () { + // Grant roles to multiple accounts + await this.manager.connect(this.admin).grantRole(ANOTHER_ROLE, this.user, 0); + await this.manager.connect(this.admin).grantRole(ANOTHER_ROLE, this.other, 0); + await this.manager.connect(this.admin).grantRole(ANOTHER_ROLE, this.admin, 0); + + await expect(this.manager.getRoleMemberCount(ANOTHER_ROLE)).to.eventually.equal(3); + + const users = [this.user.address, this.other.address, this.admin.address]; + + // Test partial enumeration + const firstTwo = await this.manager.getRoleMembers(ANOTHER_ROLE, 0, 2); + expect(firstTwo).to.have.lengthOf(2); + expect(users).to.include.members(firstTwo); + + const lastTwo = await this.manager.getRoleMembers(ANOTHER_ROLE, 1, 3); + expect(lastTwo).to.have.lengthOf(2); + expect(users).to.include.members(lastTwo); + }); + }); + + describe('target functions', function () { + it('target functions can be enumerated', async function () { + const roleId = this.roles.SOME.id; + const selectors = [ + selector('someFunction()'), + selector('anotherFunction(uint256)'), + selector('thirdFunction(address,bool)'), + ]; + + await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, selectors, roleId); + + const functionCount = await this.manager.getRoleTargetFunctionCount(roleId); + expect(functionCount).to.equal(selectors.length); + + // Test individual enumeration + const functions = []; + for (let i = 0; i < functionCount; ++i) { + functions.push(this.manager.getRoleTargetFunction(roleId, i)); + } + await expect(Promise.all(functions)).to.eventually.have.members(selectors); + + // Test batch enumeration + const batchFunctions = await this.manager.getRoleTargetFunctions(roleId, 0, functionCount); + expect([...batchFunctions]).to.have.members(selectors); + }); + + it('target function enumeration updates when roles change', async function () { + const roleId1 = this.roles.SOME.id; + const roleId2 = this.roles.SOME_ADMIN.id; + const sel = selector('testFunction()'); + + // Initially assign to roleId1 + await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, [sel], roleId1); + + await expect(this.manager.getRoleTargetFunctionCount(roleId1)).to.eventually.equal(1); + await expect(this.manager.getRoleTargetFunctionCount(roleId2)).to.eventually.equal(0); + await expect(this.manager.getRoleTargetFunction(roleId1, 0)).to.eventually.equal(sel); + + // Reassign to roleId2 + await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, [sel], roleId2); + + await expect(this.manager.getRoleTargetFunctionCount(roleId1)).to.eventually.equal(0); + await expect(this.manager.getRoleTargetFunctionCount(roleId2)).to.eventually.equal(1); + await expect(this.manager.getRoleTargetFunction(roleId2, 0)).to.eventually.equal(sel); + }); + + it('returns empty for ADMIN_ROLE target functions', async function () { + const sel = selector('adminFunction()'); + + // Set function to ADMIN_ROLE (default behavior) + await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, [sel], this.roles.ADMIN.id); + + // ADMIN_ROLE functions are not tracked + await expect(this.manager.getRoleTargetFunctionCount(this.roles.ADMIN.id)).to.eventually.equal(0); + await expect(this.manager.getRoleTargetFunctions(this.roles.ADMIN.id, 0, 10)).to.eventually.deep.equal([]); + }); + + it('returns empty for roles with no target functions', async function () { + const roleId = 888n; // Role with no functions + + await expect(this.manager.getRoleTargetFunctionCount(roleId)).to.eventually.equal(0); + await expect(this.manager.getRoleTargetFunctions(roleId, 0, 10)).to.eventually.deep.equal([]); + }); + + it('supports partial enumeration of target functions', async function () { + const roleId = this.roles.SOME.id; + const selectors = [selector('func1()'), selector('func2()'), selector('func3()'), selector('func4()')]; + + await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, selectors, roleId); + + await expect(this.manager.getRoleTargetFunctionCount(roleId)).to.eventually.equal(4); + + // Test partial enumeration + const firstTwo = await this.manager.getRoleTargetFunctions(roleId, 0, 2); + expect(firstTwo).to.have.lengthOf(2); + + const lastTwo = await this.manager.getRoleTargetFunctions(roleId, 2, 4); + expect(lastTwo).to.have.lengthOf(2); + + // Verify no overlap and complete coverage + const allFunctions = [...firstTwo, ...lastTwo]; + expect(allFunctions).to.have.members(selectors); + }); + }); + }); +} + module.exports = { shouldBehaveLikeDelayedAdminOperation, shouldBehaveLikeNotDelayedAdminOperation, shouldBehaveLikeRoleAdminOperation, shouldBehaveLikeAManagedRestrictedOperation, shouldBehaveLikeASelfRestrictedOperation, + shouldBehaveLikeAccessManagerEnumerable, }; diff --git a/test/access/manager/extensions/AccessManagerEnumerable.test.js b/test/access/manager/extensions/AccessManagerEnumerable.test.js new file mode 100644 index 00000000000..db7621d7ff4 --- /dev/null +++ b/test/access/manager/extensions/AccessManagerEnumerable.test.js @@ -0,0 +1,62 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { buildBaseRoles } = require('../../../helpers/access-manager'); +const { shouldBehaveLikeAccessManagerEnumerable } = require('../AccessManager.behavior'); + +async function fixture() { + const [admin, roleAdmin, roleGuardian, member, user, other] = await ethers.getSigners(); + + // Build roles + const roles = buildBaseRoles(); + + // Add members + roles.ADMIN.members = [admin]; + roles.SOME_ADMIN.members = [roleAdmin]; + roles.SOME_GUARDIAN.members = [roleGuardian]; + roles.SOME.members = [member]; + roles.PUBLIC.members = [admin, roleAdmin, roleGuardian, member, user, other]; + + const manager = await ethers.deployContract('$AccessManagerEnumerableMock', [admin]); + const target = await ethers.deployContract('$AccessManagedTarget', [manager]); + + for (const { id: roleId, admin, guardian, members } of Object.values(roles)) { + if (roleId === roles.PUBLIC.id) continue; // Every address belong to public and is locked + if (roleId === roles.ADMIN.id) continue; // Admin set during construction and is locked + + // Set admin role avoiding default + if (admin.id !== roles.ADMIN.id) { + await manager.$_setRoleAdmin(roleId, admin.id); + } + + // Set guardian role avoiding default + if (guardian.id !== roles.ADMIN.id) { + await manager.$_setRoleGuardian(roleId, guardian.id); + } + + // Grant role to members + for (const member of members) { + await manager.$_grantRole(roleId, member, 0, 0); + } + } + + return { + admin, + roleAdmin, + roleGuardian, + member, + user, + other, + roles, + manager, + target, + }; +} + +describe('AccessManagerEnumerable', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAccessManagerEnumerable(); +}); From b4af192a2c683990c6abc2d656a06f0845c100ea Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 10 Nov 2025 11:59:08 -0300 Subject: [PATCH 3/8] Add changeset --- .changeset/crazy-bears-flash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/crazy-bears-flash.md diff --git a/.changeset/crazy-bears-flash.md b/.changeset/crazy-bears-flash.md new file mode 100644 index 00000000000..b668edb0cc2 --- /dev/null +++ b/.changeset/crazy-bears-flash.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +Add `AccessManagerEnumerable`, an extension of `AccessManager` that allows enumerating the members of each role and the target functions each role is allowed to call. From b9e7965f03399a44f37fb67aec2dd60a6a23fde9 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 10 Nov 2025 12:12:42 -0300 Subject: [PATCH 4/8] up --- .../access/manager/extensions/AccessManagerEnumerable.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/access/manager/extensions/AccessManagerEnumerable.sol b/contracts/access/manager/extensions/AccessManagerEnumerable.sol index 256da227c48..9a05f320f0c 100644 --- a/contracts/access/manager/extensions/AccessManagerEnumerable.sol +++ b/contracts/access/manager/extensions/AccessManagerEnumerable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {IAccessManagerEnumerable} from "./IAccessManagerEnumerable.sol"; import {AccessManager} from "../AccessManager.sol"; @@ -99,7 +99,8 @@ abstract contract AccessManagerEnumerable is IAccessManagerEnumerable, AccessMan * Since the target functions for the {ADMIN_ROLE} can't be tracked exhaustively (i.e. by default, all * restricted functions), any function that is granted to the {ADMIN_ROLE} will not be tracked by this * extension. Developers may opt in for tracking the functions for the {ADMIN_ROLE} by overriding, - * though, the tracking would not be exhaustive. + * though, the tracking would not be exhaustive unless {setTargetFunctionRole} is explicitly called + * for the {ADMIN_ROLE} for each function: * * ```solidity * function _setTargetFunctionRole(address target, bytes4 selector, uint64 roleId) internal virtual override { From 2a0e9001c319f328e7ed5dffe18bdfcaf1978929 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 10 Nov 2025 12:22:53 -0300 Subject: [PATCH 5/8] Update list of functions by targets --- .../extensions/AccessManagerEnumerable.sol | 21 +++--- .../extensions/IAccessManagerEnumerable.sol | 23 +++--- test/access/manager/AccessManager.behavior.js | 75 +++++++++++++------ .../AccessManagerEnumerable.test.js | 2 + 4 files changed, 81 insertions(+), 40 deletions(-) diff --git a/contracts/access/manager/extensions/AccessManagerEnumerable.sol b/contracts/access/manager/extensions/AccessManagerEnumerable.sol index 9a05f320f0c..b8f5d926afb 100644 --- a/contracts/access/manager/extensions/AccessManagerEnumerable.sol +++ b/contracts/access/manager/extensions/AccessManagerEnumerable.sol @@ -19,7 +19,7 @@ abstract contract AccessManagerEnumerable is IAccessManagerEnumerable, AccessMan using EnumerableSet for EnumerableSet.Bytes32Set; mapping(uint64 roleId => EnumerableSet.AddressSet) private _roleMembers; - mapping(uint64 roleId => EnumerableSet.Bytes32Set) private _roleTargetFunctions; + mapping(uint64 roleId => mapping(address target => EnumerableSet.Bytes32Set)) private _roleTargetFunctions; /// @inheritdoc IAccessManagerEnumerable function getRoleMember(uint64 roleId, uint256 index) public view virtual returns (address) { @@ -37,8 +37,8 @@ abstract contract AccessManagerEnumerable is IAccessManagerEnumerable, AccessMan } /// @inheritdoc IAccessManagerEnumerable - function getRoleTargetFunction(uint64 roleId, uint256 index) public view virtual returns (bytes4) { - return bytes4(_roleTargetFunctions[roleId].at(index)); + function getRoleTargetFunction(uint64 roleId, address target, uint256 index) public view virtual returns (bytes4) { + return bytes4(_roleTargetFunctions[roleId][target].at(index)); } /* @@ -49,10 +49,11 @@ abstract contract AccessManagerEnumerable is IAccessManagerEnumerable, AccessMan */ function getRoleTargetFunctions( uint64 roleId, + address target, uint256 start, uint256 end ) public view virtual returns (bytes4[] memory) { - bytes32[] memory targetFunctions = _roleTargetFunctions[roleId].values(start, end); + bytes32[] memory targetFunctions = _roleTargetFunctions[roleId][target].values(start, end); bytes4[] memory targetFunctionSelectors; assembly ("memory-safe") { targetFunctionSelectors := targetFunctions @@ -66,8 +67,8 @@ abstract contract AccessManagerEnumerable is IAccessManagerEnumerable, AccessMan * NOTE: Given {ADMIN_ROLE} is the default role for every restricted function, passing {ADMIN_ROLE} as `roleId` will * return 0. See {_setTargetFunctionRole} for more details. */ - function getRoleTargetFunctionCount(uint64 roleId) public view virtual returns (uint256) { - return _roleTargetFunctions[roleId].length(); + function getRoleTargetFunctionCount(uint64 roleId, address target) public view virtual returns (uint256) { + return _roleTargetFunctions[roleId][target].length(); } /// @dev See {AccessManager-_grantRole}. Adds the account to the role members set. @@ -107,10 +108,10 @@ abstract contract AccessManagerEnumerable is IAccessManagerEnumerable, AccessMan * uint64 oldRoleId = getTargetFunctionRole(target, selector); * super._setTargetFunctionRole(target, selector, roleId); * if (oldRoleId == ADMIN_ROLE) { - * _roleTargetFunctions[oldRoleId].remove(bytes32(selector)); + * _roleTargetFunctions[oldRoleId][target].remove(bytes32(selector)); * } * if (roleId == ADMIN_ROLE) { - * _roleTargetFunctions[roleId].add(bytes32(selector)); + * _roleTargetFunctions[roleId][target].add(bytes32(selector)); * } * } * ``` @@ -119,10 +120,10 @@ abstract contract AccessManagerEnumerable is IAccessManagerEnumerable, AccessMan uint64 oldRoleId = getTargetFunctionRole(target, selector); super._setTargetFunctionRole(target, selector, roleId); if (oldRoleId != ADMIN_ROLE) { - _roleTargetFunctions[oldRoleId].remove(bytes32(selector)); + _roleTargetFunctions[oldRoleId][target].remove(bytes32(selector)); } if (roleId != ADMIN_ROLE) { - _roleTargetFunctions[roleId].add(bytes32(selector)); + _roleTargetFunctions[roleId][target].add(bytes32(selector)); } } } diff --git a/contracts/access/manager/extensions/IAccessManagerEnumerable.sol b/contracts/access/manager/extensions/IAccessManagerEnumerable.sol index 6d94d41d1b1..d5561f2c4d7 100644 --- a/contracts/access/manager/extensions/IAccessManagerEnumerable.sol +++ b/contracts/access/manager/extensions/IAccessManagerEnumerable.sol @@ -43,8 +43,8 @@ interface IAccessManagerEnumerable is IAccessManager { function getRoleMemberCount(uint64 roleId) external view returns (uint256); /** - * @dev Returns one of the target function selectors that require `roleId`. `index` must be a - * value between 0 and {getRoleTargetFunctionCount}, non-inclusive. + * @dev Returns one of the target function selectors that require `roleId` for the given `target`. + * `index` must be a value between 0 and {getRoleTargetFunctionCount}, non-inclusive. * * Target function selectors are not sorted in any particular way, and their ordering may * change at any point. @@ -54,11 +54,11 @@ interface IAccessManagerEnumerable is IAccessManager { * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] * for more information. */ - function getRoleTargetFunction(uint64 roleId, uint256 index) external view returns (bytes4); + function getRoleTargetFunction(uint64 roleId, address target, uint256 index) external view returns (bytes4); /** - * @dev Returns a range of target function selectors that require `roleId`. `start` and `end` define the range bounds. - * `start` is inclusive and `end` is exclusive. + * @dev Returns a range of target function selectors that require `roleId` for the given `target`. + * `start` and `end` define the range bounds. `start` is inclusive and `end` is exclusive. * * Target function selectors are not sorted in any particular way, and their ordering may * change at any point. @@ -68,11 +68,16 @@ interface IAccessManagerEnumerable is IAccessManager { * 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 getRoleTargetFunctions(uint64 roleId, uint256 start, uint256 end) external view returns (bytes4[] memory); + function getRoleTargetFunctions( + uint64 roleId, + address target, + uint256 start, + uint256 end + ) external view returns (bytes4[] memory); /** - * @dev Returns the number of target function selectors that require `roleId`. Can be used - * together with {getRoleTargetFunction} to enumerate all target functions for a role. + * @dev Returns the number of target function selectors that require `roleId` for the given `target`. + * Can be used together with {getRoleTargetFunction} to enumerate all target functions for a role on a specific target. */ - function getRoleTargetFunctionCount(uint64 roleId) external view returns (uint256); + function getRoleTargetFunctionCount(uint64 roleId, address target) external view returns (uint256); } diff --git a/test/access/manager/AccessManager.behavior.js b/test/access/manager/AccessManager.behavior.js index 5d63ca2a7a7..f3e68301713 100644 --- a/test/access/manager/AccessManager.behavior.js +++ b/test/access/manager/AccessManager.behavior.js @@ -327,86 +327,119 @@ function shouldBehaveLikeAccessManagerEnumerable() { describe('target functions', function () { it('target functions can be enumerated', async function () { const roleId = this.roles.SOME.id; + const target = this.target; const selectors = [ selector('someFunction()'), selector('anotherFunction(uint256)'), selector('thirdFunction(address,bool)'), ]; - await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, selectors, roleId); + await this.manager.connect(this.admin).setTargetFunctionRole(target, selectors, roleId); - const functionCount = await this.manager.getRoleTargetFunctionCount(roleId); + const functionCount = await this.manager.getRoleTargetFunctionCount(roleId, target); expect(functionCount).to.equal(selectors.length); // Test individual enumeration const functions = []; for (let i = 0; i < functionCount; ++i) { - functions.push(this.manager.getRoleTargetFunction(roleId, i)); + functions.push(this.manager.getRoleTargetFunction(roleId, target, i)); } await expect(Promise.all(functions)).to.eventually.have.members(selectors); // Test batch enumeration - const batchFunctions = await this.manager.getRoleTargetFunctions(roleId, 0, functionCount); + const batchFunctions = await this.manager.getRoleTargetFunctions(roleId, target, 0, functionCount); expect([...batchFunctions]).to.have.members(selectors); }); it('target function enumeration updates when roles change', async function () { const roleId1 = this.roles.SOME.id; const roleId2 = this.roles.SOME_ADMIN.id; + const target = this.target; const sel = selector('testFunction()'); // Initially assign to roleId1 - await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, [sel], roleId1); + await this.manager.connect(this.admin).setTargetFunctionRole(target, [sel], roleId1); - await expect(this.manager.getRoleTargetFunctionCount(roleId1)).to.eventually.equal(1); - await expect(this.manager.getRoleTargetFunctionCount(roleId2)).to.eventually.equal(0); - await expect(this.manager.getRoleTargetFunction(roleId1, 0)).to.eventually.equal(sel); + await expect(this.manager.getRoleTargetFunctionCount(roleId1, target)).to.eventually.equal(1); + await expect(this.manager.getRoleTargetFunctionCount(roleId2, target)).to.eventually.equal(0); + await expect(this.manager.getRoleTargetFunction(roleId1, target, 0)).to.eventually.equal(sel); // Reassign to roleId2 - await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, [sel], roleId2); + await this.manager.connect(this.admin).setTargetFunctionRole(target, [sel], roleId2); - await expect(this.manager.getRoleTargetFunctionCount(roleId1)).to.eventually.equal(0); - await expect(this.manager.getRoleTargetFunctionCount(roleId2)).to.eventually.equal(1); - await expect(this.manager.getRoleTargetFunction(roleId2, 0)).to.eventually.equal(sel); + await expect(this.manager.getRoleTargetFunctionCount(roleId1, target)).to.eventually.equal(0); + await expect(this.manager.getRoleTargetFunctionCount(roleId2, target)).to.eventually.equal(1); + await expect(this.manager.getRoleTargetFunction(roleId2, target, 0)).to.eventually.equal(sel); }); it('returns empty for ADMIN_ROLE target functions', async function () { + const target = this.target; const sel = selector('adminFunction()'); // Set function to ADMIN_ROLE (default behavior) - await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, [sel], this.roles.ADMIN.id); + await this.manager.connect(this.admin).setTargetFunctionRole(target, [sel], this.roles.ADMIN.id); // ADMIN_ROLE functions are not tracked - await expect(this.manager.getRoleTargetFunctionCount(this.roles.ADMIN.id)).to.eventually.equal(0); - await expect(this.manager.getRoleTargetFunctions(this.roles.ADMIN.id, 0, 10)).to.eventually.deep.equal([]); + await expect(this.manager.getRoleTargetFunctionCount(this.roles.ADMIN.id, target)).to.eventually.equal(0); + await expect(this.manager.getRoleTargetFunctions(this.roles.ADMIN.id, target, 0, 10)).to.eventually.deep.equal( + [], + ); }); it('returns empty for roles with no target functions', async function () { const roleId = 888n; // Role with no functions + const target = this.target; - await expect(this.manager.getRoleTargetFunctionCount(roleId)).to.eventually.equal(0); - await expect(this.manager.getRoleTargetFunctions(roleId, 0, 10)).to.eventually.deep.equal([]); + await expect(this.manager.getRoleTargetFunctionCount(roleId, target)).to.eventually.equal(0); + await expect(this.manager.getRoleTargetFunctions(roleId, target, 0, 10)).to.eventually.deep.equal([]); }); it('supports partial enumeration of target functions', async function () { const roleId = this.roles.SOME.id; + const target = this.target; const selectors = [selector('func1()'), selector('func2()'), selector('func3()'), selector('func4()')]; - await this.manager.connect(this.admin).setTargetFunctionRole(this.manager, selectors, roleId); + await this.manager.connect(this.admin).setTargetFunctionRole(target, selectors, roleId); - await expect(this.manager.getRoleTargetFunctionCount(roleId)).to.eventually.equal(4); + await expect(this.manager.getRoleTargetFunctionCount(roleId, target)).to.eventually.equal(4); // Test partial enumeration - const firstTwo = await this.manager.getRoleTargetFunctions(roleId, 0, 2); + const firstTwo = await this.manager.getRoleTargetFunctions(roleId, target, 0, 2); expect(firstTwo).to.have.lengthOf(2); - const lastTwo = await this.manager.getRoleTargetFunctions(roleId, 2, 4); + const lastTwo = await this.manager.getRoleTargetFunctions(roleId, target, 2, 4); expect(lastTwo).to.have.lengthOf(2); // Verify no overlap and complete coverage const allFunctions = [...firstTwo, ...lastTwo]; expect(allFunctions).to.have.members(selectors); }); + + it('distinguishes between different targets', async function () { + const roleId = this.roles.SOME.id; + const target1 = this.target; + const target2 = this.target2; + const sel1 = selector('target1Function()'); + const sel2 = selector('target2Function()'); + + // Set different functions for the same role on different targets + await this.manager.connect(this.admin).setTargetFunctionRole(target1, [sel1], roleId); + await this.manager.connect(this.admin).setTargetFunctionRole(target2, [sel2], roleId); + + // Each target should have its own function tracked + await expect(this.manager.getRoleTargetFunctionCount(roleId, target1)).to.eventually.equal(1); + await expect(this.manager.getRoleTargetFunctionCount(roleId, target2)).to.eventually.equal(1); + + await expect(this.manager.getRoleTargetFunction(roleId, target1, 0)).to.eventually.equal(sel1); + await expect(this.manager.getRoleTargetFunction(roleId, target2, 0)).to.eventually.equal(sel2); + + // Functions should be isolated per target + const target1Functions = await this.manager.getRoleTargetFunctions(roleId, target1, 0, 1); + const target2Functions = await this.manager.getRoleTargetFunctions(roleId, target2, 0, 1); + + expect(target1Functions).to.deep.equal([sel1]); + expect(target2Functions).to.deep.equal([sel2]); + }); }); }); } diff --git a/test/access/manager/extensions/AccessManagerEnumerable.test.js b/test/access/manager/extensions/AccessManagerEnumerable.test.js index db7621d7ff4..d0f3250331d 100644 --- a/test/access/manager/extensions/AccessManagerEnumerable.test.js +++ b/test/access/manager/extensions/AccessManagerEnumerable.test.js @@ -19,6 +19,7 @@ async function fixture() { const manager = await ethers.deployContract('$AccessManagerEnumerableMock', [admin]); const target = await ethers.deployContract('$AccessManagedTarget', [manager]); + const target2 = await ethers.deployContract('$AccessManagedTarget', [manager]); for (const { id: roleId, admin, guardian, members } of Object.values(roles)) { if (roleId === roles.PUBLIC.id) continue; // Every address belong to public and is locked @@ -50,6 +51,7 @@ async function fixture() { roles, manager, target, + target2, }; } From ab194a70dde09cfa34f7de4ebd39d0bca88d916f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 10 Nov 2025 12:39:33 -0300 Subject: [PATCH 6/8] Add docs --- contracts/access/README.adoc | 5 +++ docs/modules/ROOT/pages/access-control.adoc | 47 +++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/contracts/access/README.adoc b/contracts/access/README.adoc index b89865b2c17..ac5c08994f4 100644 --- a/contracts/access/README.adoc +++ b/contracts/access/README.adoc @@ -6,6 +6,7 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ This directory provides ways to restrict who can access the functions of a contract or when they can do it. - {AccessManager} is a full-fledged access control solution for smart contract systems. Allows creating and assigning multiple hierarchical roles with execution delays for each account across various contracts. +- {AccessManagerEnumerable} is an extension to {AccessManager} that enumerates role members and target functions each role can call. - {AccessManaged} delegates its access control to an authority that dictates the permissions of the managed contract. It's compatible with an AccessManager as an authority. - {AccessControl} provides a per-contract role based access control mechanism. Multiple hierarchical roles can be created and assigned each to multiple accounts within the same instance. - {Ownable} is a simpler mechanism with a single owner "role" that can be assigned to a single account. This simpler mechanism can be useful for quick tests but projects with production concerns are likely to outgrow it. @@ -38,6 +39,10 @@ This directory provides ways to restrict who can access the functions of a contr {{AccessManager}} +{{IAccessManagerEnumerable}} + +{{AccessManagerEnumerable}} + {{IAccessManaged}} {{AccessManaged}} diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index d8b8cdb78c1..e4ed72a019e 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -272,6 +272,53 @@ The delayed admin actions are: * Closing or opening a target via xref:api:access.adoc#AccessManager-setTargetClosed-address-bool-[`setTargetClosed`]. * Changing permissions of whether a role can call a target function with xref:api:access.adoc#AccessManager-setTargetFunctionRole-address-bytes4---uint64-[`setTargetFunctionRole`]. +=== Querying Privileged Accounts + +Similar to `AccessControl`, accounts might be granted and revoked roles dynamically in an `AccessManager`, making it challenging to determine which accounts hold a particular role at any given time. This capability is essential for proving certain properties about a system, such as verifying that an administrative role is held by a multisig or DAO, or that a certain role has been completely removed to disable associated functionality. + +The base `AccessManager` contract provides comprehensive role-based access control but does not support on-chain enumeration of role members or target function permissions by default. To track which accounts hold roles and which functions are assigned to roles, you should rely on the xref:api:access.adoc#AccessManager-RoleGranted-uint64-address-uint32-uint48-bool-[RoleGranted], xref:api:access.adoc#AccessManager-RoleRevoked-uint64-address-[RoleRevoked], and xref:api:access.adoc#AccessManager-TargetFunctionRoleUpdated-address-bytes4-uint64-[TargetFunctionRoleUpdated] events, which can be processed off-chain. + +If on-chain enumeration is required, you can use the xref:api:access.adoc#AccessManagerEnumerable[`AccessManagerEnumerable`] extension. This extension uses `EnumerableSet` internally and provides the following functions for role members: + +* {AccessManagerEnumerable-getRoleMemberCount} +* {AccessManagerEnumerable-getRoleMember} +* {AccessManagerEnumerable-getRoleMembers} + +And these functions for target function permissions: + +* {AccessManagerEnumerable-getRoleTargetFunctionCount} +* {AccessManagerEnumerable-getRoleTargetFunction} +* {AccessManagerEnumerable-getRoleTargetFunctions} + +These can be used to iterate over the accounts that have been granted a role and the functions that a role is allowed to call on specific targets: + +```javascript +// Enumerate role members +const minterCount = await accessManager.getRoleMemberCount(MINTER_ROLE); + +const members = []; +for (let i = 0; i < minterCount; ++i) { + members.push(await accessManager.getRoleMember(MINTER_ROLE, i)); +} + +// Or get all members at once +const allMembers = await accessManager.getRoleMembers(MINTER_ROLE, 0, minterCount); + +// Enumerate target functions for a role +const target = await myToken.getAddress(); +const functionCount = await accessManager.getRoleTargetFunctionCount(MINTER_ROLE, target); + +const functions = []; +for (let i = 0; i < functionCount; ++i) { + functions.push(await accessManager.getRoleTargetFunction(MINTER_ROLE, target, i)); +} + +// Or get all functions at once +const allFunctions = await accessManager.getRoleTargetFunctions(MINTER_ROLE, target, 0, functionCount); +``` + +Note that target function enumeration is organized per target contract, allowing you to query which functions a role can access on each specific target separately. This provides fine-grained visibility into the permission structure across your entire system of managed contracts. + === Using with Ownable Contracts already inheriting from xref:api:access.adoc#Ownable[`Ownable`] can migrate to AccessManager by transferring ownership to the manager. After that, all calls to functions with the `onlyOwner` modifier should be called through the manager's xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] function, even if the caller doesn't require a delay. From dad105cec2e65a484a40f33a316aeb657f58cba7 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 10 Nov 2025 15:53:16 -0300 Subject: [PATCH 7/8] Update doc references --- docs/modules/ROOT/pages/access-control.adoc | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index e4ed72a019e..37dd6024c75 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -96,8 +96,8 @@ The base `AccessControl` contract provides role-based access control, but it doe This contract uses `EnumerableSet` internally and provides the following functions: -* {AccessControlEnumerable-getRoleMemberCount} -* {AccessControlEnumerable-getRoleMember} +* xref:api:access.adoc#AccessControl-getRoleMemberCount[`getRoleMemberCount`] +* xref:api:access.adoc#AccessControl-getRoleMember[`getRoleMember`] These can be used to iterate over the accounts that have been granted a role: @@ -280,15 +280,15 @@ The base `AccessManager` contract provides comprehensive role-based access contr If on-chain enumeration is required, you can use the xref:api:access.adoc#AccessManagerEnumerable[`AccessManagerEnumerable`] extension. This extension uses `EnumerableSet` internally and provides the following functions for role members: -* {AccessManagerEnumerable-getRoleMemberCount} -* {AccessManagerEnumerable-getRoleMember} -* {AccessManagerEnumerable-getRoleMembers} +* xref:api:access.adoc#AccessManager-getRoleMemberCount[`getRoleMemberCount`] +* xref:api:access.adoc#AccessManager-getRoleMember[`getRoleMember`] +* xref:api:access.adoc#AccessManager-getRoleMembers[`getRoleMembers`] And these functions for target function permissions: -* {AccessManagerEnumerable-getRoleTargetFunctionCount} -* {AccessManagerEnumerable-getRoleTargetFunction} -* {AccessManagerEnumerable-getRoleTargetFunctions} +* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunctionCount[`getRoleTargetFunctionCount`] +* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunction[`getRoleTargetFunction`] +* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunctions[`getRoleTargetFunctions`] These can be used to iterate over the accounts that have been granted a role and the functions that a role is allowed to call on specific targets: From ece797a17570acd3affa654dbe5920dece98c2f2 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 10 Nov 2025 16:02:06 -0300 Subject: [PATCH 8/8] up --- contracts/access/README.adoc | 10 ++++++---- docs/modules/ROOT/pages/access-control.adoc | 17 +++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/contracts/access/README.adoc b/contracts/access/README.adoc index ac5c08994f4..ec837cdbd90 100644 --- a/contracts/access/README.adoc +++ b/contracts/access/README.adoc @@ -39,12 +39,14 @@ This directory provides ways to restrict who can access the functions of a contr {{AccessManager}} -{{IAccessManagerEnumerable}} - -{{AccessManagerEnumerable}} - {{IAccessManaged}} {{AccessManaged}} {{AuthorityUtils}} + +=== Extensions + +{{IAccessManagerEnumerable}} + +{{AccessManagerEnumerable}} diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index 37dd6024c75..4ac4e4e480f 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -96,8 +96,9 @@ The base `AccessControl` contract provides role-based access control, but it doe This contract uses `EnumerableSet` internally and provides the following functions: -* xref:api:access.adoc#AccessControl-getRoleMemberCount[`getRoleMemberCount`] -* xref:api:access.adoc#AccessControl-getRoleMember[`getRoleMember`] +* xref:api:access.adoc#AccessControlEnumerable-getRoleMemberCount-bytes32-[`getRoleMemberCount`] +* xref:api:access.adoc#AccessControlEnumerable-getRoleMember-bytes32-uint256-[`getRoleMember`] +* xref:api:access.adoc#AccessControlEnumerable-getRoleMembers-bytes32-[`getRoleMembers`] These can be used to iterate over the accounts that have been granted a role: @@ -280,15 +281,15 @@ The base `AccessManager` contract provides comprehensive role-based access contr If on-chain enumeration is required, you can use the xref:api:access.adoc#AccessManagerEnumerable[`AccessManagerEnumerable`] extension. This extension uses `EnumerableSet` internally and provides the following functions for role members: -* xref:api:access.adoc#AccessManager-getRoleMemberCount[`getRoleMemberCount`] -* xref:api:access.adoc#AccessManager-getRoleMember[`getRoleMember`] -* xref:api:access.adoc#AccessManager-getRoleMembers[`getRoleMembers`] +* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunctionCount-uint64-address-[`getRoleMemberCount`] +* xref:api:access.adoc#AccessManagerEnumerable-getRoleMember-uint64-uint256-[`getRoleMember`] +* xref:api:access.adoc#AccessManagerEnumerable-getRoleMembers-uint64-uint256-uint256-[`getRoleMembers`] And these functions for target function permissions: -* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunctionCount[`getRoleTargetFunctionCount`] -* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunction[`getRoleTargetFunction`] -* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunctions[`getRoleTargetFunctions`] +* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunctionCount-uint64-address-[`getRoleTargetFunctionCount`] +* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunction-uint64-address-uint256-[`getRoleTargetFunction`] +* xref:api:access.adoc#AccessManagerEnumerable-getRoleTargetFunctions-uint64-address-uint256-uint256-[`getRoleTargetFunctions`] These can be used to iterate over the accounts that have been granted a role and the functions that a role is allowed to call on specific targets: