Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use context-based role assignments to implement the permission model #19

Open
wants to merge 25 commits into
base: main
Choose a base branch
from

Conversation

hiddentao
Copy link
Contributor

@hiddentao hiddentao commented Feb 23, 2025

Notes:

  • All new ACL that allows for context-based role setting and other fun features. Fully tested!
  • The transfer lock is complex to do using roles because it would require going into the ERC1155 too to handle approvals etc. So I opted for a flag instead for that one.

@hiddentao hiddentao marked this pull request as draft February 23, 2025 08:09
@coveralls
Copy link

coveralls commented Feb 23, 2025

Pull Request Test Coverage Report for Build 13805766124

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 94 of 180 (52.22%) changed or added relevant lines in 9 files are covered.
  • 4 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-3.2%) to 54.758%

Changes Missing Coverage Covered Lines Changed/Added Lines %
contracts/src/registry/RootRegistry.sol 2 3 66.67%
contracts/src/registry/PermissionedRegistry.sol 2 4 50.0%
contracts/src/registry/NameWrapperRegistry.sol 0 83 0.0%
Files with Coverage Reduction New Missed Lines %
contracts/src/registry/RootRegistry.sol 1 75.0%
contracts/src/registry/PermissionedRegistry.sol 3 73.68%
Totals Coverage Status
Change from base Build 13351832110: -3.2%
Covered Lines: 278
Relevant Lines: 495

💛 - Coveralls

@hiddentao hiddentao requested a review from Arachnid February 23, 2025 09:24
@hiddentao hiddentao requested a review from Arachnid February 26, 2025 11:19
feat: add _transferRole method
@hiddentao
Copy link
Contributor Author

hiddentao commented Feb 28, 2025

@Arachnid Given that we only need to transfer two roles to a new registrar (registrar and renew) I felt that it was easier to do that manually whilst keeping the current storage approach in EnhancedAccessControl, i.e. not using a bitmap. Even if we use a Bitmap then keeping the "revoke all assignments of this role within this resource" O(1) feature would still require versioning of assignments, meaning that we'd still be doing a lot of complex storage work in access control.

Plus, role transfers only work across a particular context. There's no way to transfer all of a user's roles in all contexts. I suppose if we implement hierarchical contexts of some sort we could perhaps do this. But again, this complicates the access control storage.

Happy to discuss this further over calls.

Comment on lines +30 to +34
/**
* @dev admin role that controls a given role.
* RoleId -> AdminRoleId
*/
mapping(uint256 role => uint256 adminRole) private _adminRoles;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simply say that the upper 128 bits are admin roles and the lower 128 bits are user roles, and thus remove the need to map them explicitly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's possible that a role can be a role as well as an admin role - e.g registrar can be admin roles for setting the renewer admin role, even though registrars are themselves roles.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's in harm in representing those with separate bits, though? If they're typically used together, the inheriting contract can simply grant or revoke both bits together.

Comment on lines +42 to +46
/**
* @dev locked roles within a resource stored as a bitmap.
* Resource -> LockedRoleBitmap
*/
mapping(bytes32 resource => uint256 lockedRoleBitmap) private _lockedRoles;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? If a role for a given resource has no admins, it's locked implicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that the DEFAULT_ADMIN_ROLE can always revoke a role, thus I figured we needed an explicit locking mechanism to prevent that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the admin-bit-for-each-role I suggested above, there is no DEFAULT_ADMIN_ROLE - only individual admins for each role.

*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 resource, uint256 role, address account) public virtual canGrantRole(resource, role) returns (bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function grantRole(bytes32 resource, uint256 role, address account) public virtual canGrantRole(resource, role) returns (bool) {
function grantRoles(bytes32 resource, uint256 role, address account) public virtual canGrantRole(resource, role) returns (bool) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't as straightforward since we need to check that the caller has the admin role for the given role - and we don't want to be looping through and checking the caller is an admin for every role passed in.

The alternative is to be able to set an admin for a bitmap of roles, but then that semantically doesn't make any sense.

Hence why granting/revoking one role at a time makes the most sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do the admin-role-per-role I suggested above, this is doable with simple bitmasking. Eg:

uint256 caller_roles = getRoles(resource, msg.sender);
uint256 caller_admin_roles = (caller_roles & 0xffffffffffffffffffffffffffffffff) | (caller_roles >> 128);
role &= caller_admin_roles;

This will take the role input and mask out any roles the caller doesn't have permission to grant.

*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 resource, uint256 role, address account) public virtual canGrantRole(resource, role) returns (bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function revokeRole(bytes32 resource, uint256 role, address account) public virtual canGrantRole(resource, role) returns (bool) {
function revokeRoles(bytes32 resource, uint256 role, address account) public virtual canGrantRole(resource, role) returns (bool) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't as straightforward since we need to check that the caller has the admin role for the given role - and we don't want to be looping through and checking the caller is an admin for every role passed in.

The alternative is to be able to set an admin for a bitmap of roles, but then that semantically doesn't make any sense.

Hence why granting/revoking one role at a time makes the most sense.

Comment on lines 166 to 190
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*
* Returns `true` if the role was revoked, `false` otherwise.
*/
function renounceRole(bytes32 resource, uint256 role, address callerConfirmation) public virtual returns (bool) {
if (callerConfirmation != _msgSender()) {
revert EnhancedAccessControlBadConfirmation();
}

return _revokeRoles(resource, role, callerConfirmation);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should omit this; if an account is an admin for the role, they can revoke both the role and their own admin status, but having this makes it impossible to 'lock open' a permission.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand what you mean by "lock open" here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revoke all admin access over a role, without revoking the role itself for everyone - meaning it can still be done, but the set of accounts that can do it can't be changed any longer.

@hiddentao hiddentao marked this pull request as ready for review March 11, 2025 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants