The EnhancedAccessControl abstract contract is the foundation of ENS v2’s permission system. It provides a gas-efficient, bitmap-based implementation of resource-scoped roles with admin role pairing.
Architecture
Storage Structure
EAC uses two primary storage mappings:
// User roles within a resource stored as a bitmap
// Resource -> User -> RoleBitmap
mapping ( uint256 resource => mapping ( address account => uint256 roleBitmap)) private _roles;
// The number of assignees for a given role in a given resource
// Each role's count is represented by 4 bits, in little-endian order
mapping ( uint256 resource => uint256 roleCount) private _roleCount;
Role Bitmap Layout
Roles are stored in a uint256 bitmap with a specific structure:
Regular Roles (Lower 128 bits)
Admin Roles (Upper 128 bits)
// Bits 0-3: Role 0
// Bits 4-7: Role 1
// Bits 8-11: Role 2
// Bits 12-15: Role 3
// ...
// Bits 124-127: Role 31
This layout enables efficient admin role lookup:
uint256 adminRole = role << 128 ;
Core Functions
Granting Roles
For Specific Resources
function grantRoles (
uint256 resource ,
uint256 roleBitmap ,
address account
) public returns ( bool );
The resource ID to grant roles within (cannot be ROOT_RESOURCE)
Bitmap of roles to grant (can include multiple roles via bitwise OR)
The address to grant roles to
true if roles were granted, false if account already had all roles
Requirements:
Caller must hold the admin roles for all roles in roleBitmap
resource cannot be ROOT_RESOURCE (use grantRootRoles instead)
Cannot exceed 15 assignees per role
Example:
// Grant multiple roles at once
uint256 roles = ROLE_SET_RESOLVER | ROLE_SET_SUBREGISTRY;
registry. grantRoles (tokenId, roles, operator);
For Root Resource
function grantRootRoles (
uint256 roleBitmap ,
address account
) public returns ( bool );
Grants roles at the ROOT_RESOURCE level, making them effective across all resources.
// Grant contract-wide resolver permission
registry. grantRootRoles (
RegistryRolesLib.ROLE_SET_RESOLVER,
admin
);
Revoking Roles
For Specific Resources
function revokeRoles (
uint256 resource ,
uint256 roleBitmap ,
address account
) public returns ( bool );
Requirements:
Caller must hold the admin roles for all roles in roleBitmap
resource cannot be ROOT_RESOURCE (use revokeRootRoles instead)
Example:
// Revoke resolver permission
registry. revokeRoles (
tokenId,
RegistryRolesLib.ROLE_SET_RESOLVER,
operator
);
For Root Resource
function revokeRootRoles (
uint256 roleBitmap ,
address account
) public returns ( bool );
Admin roles can be revoked from yourself, enabling patterns like soulbound tokens by revoking ROLE_CAN_TRANSFER_ADMIN.
Checking Roles
Resource-Specific Check
function hasRoles (
uint256 resource ,
uint256 roleBitmap ,
address account
) public view returns ( bool );
Returns true if account has all roles in roleBitmap for either the specific resource or the ROOT_RESOURCE.
// Check if user can set resolver
if (registry. hasRoles (tokenId, ROLE_SET_RESOLVER, user)) {
// User has permission
}
Root-Only Check
function hasRootRoles (
uint256 roleBitmap ,
address account
) public view returns ( bool );
Returns true only if roles are granted at the ROOT_RESOURCE level (does not check resource-specific roles).
Get Role Bitmap
function roles (
uint256 resource ,
address account
) public view returns ( uint256 );
Returns the complete role bitmap for an account in a specific resource.
uint256 userRoles = registry. roles (tokenId, user);
if (userRoles & ROLE_SET_RESOLVER != 0 ) {
// User has resolver role
}
Get Assignee Count
function getAssigneeCount (
uint256 resource ,
uint256 roleBitmap
) public view returns ( uint256 counts , uint256 mask );
Returns the number of assignees for each role as a packed 4-bit array.
( uint256 counts, uint256 mask) = registry. getAssigneeCount (
tokenId,
ROLE_SET_RESOLVER
);
Modifiers
onlyRoles
Restricts function access to accounts with specific roles:
function setResolver ( uint256 tokenId , address resolver )
public
onlyRoles ( tokenId , ROLE_SET_RESOLVER )
{
// Implementation
}
onlyRootRoles
Restricts access to accounts with root-level roles:
function setParent ( IRegistry parent , string memory label )
public
onlyRootRoles ( ROLE_SET_PARENT )
{
// Implementation
}
canGrantRoles
Ensures caller can grant the specified roles:
function customGrantRoles ( uint256 resource , uint256 roleBitmap , address account )
public
canGrantRoles ( resource , roleBitmap )
{
// Custom grant logic
}
canRevokeRoles
Ensures caller can revoke the specified roles:
function customRevokeRoles ( uint256 resource , uint256 roleBitmap , address account )
public
canRevokeRoles ( resource , roleBitmap )
{
// Custom revoke logic
}
Internal Functions
Derived contracts have access to internal functions for custom logic:
_grantRoles
function _grantRoles (
uint256 resource ,
uint256 roleBitmap ,
address account ,
bool executeCallbacks
) internal returns ( bool );
Grants roles without permission checks. Set executeCallbacks to false to skip _onRolesGranted callback.
_revokeRoles
function _revokeRoles (
uint256 resource ,
uint256 roleBitmap ,
address account ,
bool executeCallbacks
) internal returns ( bool );
Revokes roles without permission checks.
_transferRoles
function _transferRoles (
uint256 resource ,
address srcAccount ,
address dstAccount ,
bool executeCallbacks
) internal ;
Transfers all roles from one account to another within a resource. Used during NFT transfers.
_revokeAllRoles
function _revokeAllRoles (
uint256 resource ,
address account ,
bool executeCallbacks
) internal returns ( bool );
Revokes all roles (including admin roles) from an account.
Lifecycle Hooks
Override these hooks to implement custom behavior:
_onRolesGranted
function _onRolesGranted (
uint256 resource ,
address account ,
uint256 oldRoles ,
uint256 newRoles ,
uint256 roleBitmap
) internal virtual ;
Called after roles are granted. In PermissionedRegistry, this triggers token regeneration.
_onRolesRevoked
function _onRolesRevoked (
uint256 resource ,
address account ,
uint256 oldRoles ,
uint256 newRoles ,
uint256 roleBitmap
) internal virtual ;
Called after roles are revoked. Also triggers token regeneration in registries.
Events
EACRolesChanged
event EACRolesChanged (
uint256 indexed resource ,
address indexed account ,
uint256 oldRoleBitmap ,
uint256 newRoleBitmap
);
Emitted whenever roles are granted or revoked.
Parameters:
resource: The resource where roles changed
account: The account whose roles changed
oldRoleBitmap: The previous role bitmap
newRoleBitmap: The new role bitmap
Error Reference
EACUnauthorizedAccountRoles
error EACUnauthorizedAccountRoles (
uint256 resource,
uint256 roleBitmap,
address account
);
Thrown when an account lacks required roles for an operation.
Example: /home/daytona/workspace/source/contracts/src/access-control/EnhancedAccessControl.sol:384
EACCannotGrantRoles
error EACCannotGrantRoles (
uint256 resource,
uint256 roleBitmap,
address account
);
Thrown when attempting to grant roles without the necessary admin roles.
EACCannotRevokeRoles
error EACCannotRevokeRoles (
uint256 resource,
uint256 roleBitmap,
address account
);
Thrown when attempting to revoke roles without the necessary admin roles.
EACRootResourceNotAllowed
error EACRootResourceNotAllowed ();
Thrown when using grantRoles or revokeRoles with ROOT_RESOURCE. Use grantRootRoles or revokeRootRoles instead.
EACMaxAssignees
error EACMaxAssignees ( uint256 resource, uint256 role);
Thrown when trying to grant a role that already has 15 assignees.
EACMinAssignees
error EACMinAssignees ( uint256 resource, uint256 role);
Thrown when role count underflows (internal error).
EACInvalidRoleBitmap
error EACInvalidRoleBitmap ( uint256 roleBitmap);
Thrown when a role bitmap contains bits outside valid role positions.
EACInvalidAccount
error EACInvalidAccount ();
Thrown when attempting to grant roles to the zero address.
Implementation Details
Permission Calculation
The hasRoles function implements inheritance:
function hasRoles (
uint256 resource ,
uint256 roleBitmap ,
address account
) public view virtual returns ( bool ) {
return (
(_roles[ROOT_RESOURCE][account] | _roles[resource][account]) & roleBitmap
) == roleBitmap;
}
This bitwise OR combines root and resource-specific roles, then checks if all required roles are present.
Admin Role Lookup
Admin roles are stored 128 bits higher than their corresponding regular roles:
uint256 adminRole = role << 128 ;
This enables O(1) admin role lookup and validation.
Assignee Counting
Each role’s assignee count occupies 4 bits in the _roleCount mapping, limiting each role to 15 assignees. This uses the role bitmap itself as the increment/decrement value:
if (isGrant) {
_roleCount[resource] += roleBitmap;
} else {
_roleCount[resource] -= roleBitmap;
}
Best Practices
Combine Roles : When granting multiple roles, combine them with bitwise OR in a single transaction to save gas:uint256 roles = ROLE_SET_RESOLVER | ROLE_SET_SUBREGISTRY | ROLE_RENEW;
registry. grantRoles (tokenId, roles, operator);
Root Roles Are Powerful : Granting roles at the root level gives permissions across all resources. Use sparingly for trusted administrators only.
Event-Based Indexing : All role changes emit events. Build indexers that track these events to maintain an off-chain permission database.
IEnhancedAccessControl : Public interface for the access control system
EACBaseRolesLib : Library with role constants (ALL_ROLES, ADMIN_ROLES)
RegistryRolesLib : Registry-specific role definitions
Next Steps
Registry Roles Learn about specific roles in registry contracts
Permission Inheritance Understand how root permissions cascade to resources