Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/crazy-bears-flash.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions contracts/access/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -43,3 +44,9 @@ This directory provides ways to restrict who can access the functions of a contr
{{AccessManaged}}

{{AuthorityUtils}}

=== Extensions

{{IAccessManagerEnumerable}}

{{AccessManagerEnumerable}}
129 changes: 129 additions & 0 deletions contracts/access/manager/extensions/AccessManagerEnumerable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {IAccessManagerEnumerable} from "./IAccessManagerEnumerable.sol";
import {AccessManager} from "../AccessManager.sol";
import {EnumerableSet} from "../../../utils/structs/EnumerableSet.sol";

/**
* @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 => mapping(address target => 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, address target, uint256 index) public view virtual returns (bytes4) {
return bytes4(_roleTargetFunctions[roleId][target].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,
address target,
uint256 start,
uint256 end
) public view virtual returns (bytes4[] memory) {
bytes32[] memory targetFunctions = _roleTargetFunctions[roleId][target].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, address target) public view virtual returns (uint256) {
return _roleTargetFunctions[roleId][target].length();
}

/// @dev See {AccessManager-_grantRole}. Adds the account to the role members set.
function _grantRole(
uint64 roleId,
address account,
uint32 grantDelay,
uint32 executionDelay
) internal virtual override returns (bool) {
bool granted = super._grantRole(roleId, account, grantDelay, executionDelay);
if (granted) {
_roleMembers[roleId].add(account);
}
return granted;
}

/// @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);
}
return revoked;
}

/**
* @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 unless {setTargetFunctionRole} is explicitly called
* for the {ADMIN_ROLE} for each function:
*
* ```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][target].remove(bytes32(selector));
* }
* if (roleId == ADMIN_ROLE) {
* _roleTargetFunctions[roleId][target].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][target].remove(bytes32(selector));
}
if (roleId != ADMIN_ROLE) {
_roleTargetFunctions[roleId][target].add(bytes32(selector));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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` 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.
*
* 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, address target, uint256 index) external view returns (bytes4);

/**
* @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.
*
* 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,
address target,
uint256 start,
uint256 end
) external view returns (bytes4[] memory);

/**
* @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, address target) external view returns (uint256);
}
29 changes: 29 additions & 0 deletions contracts/mocks/AccessManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}
52 changes: 50 additions & 2 deletions docs/modules/ROOT/pages/access-control.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

* {AccessControlEnumerable-getRoleMemberCount}
* {AccessControlEnumerable-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:

Expand Down Expand Up @@ -272,6 +273,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:

* 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-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:

```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.
Expand Down
Loading