Skip to main content
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:
// 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);
resource
uint256
The resource ID to grant roles within (cannot be ROOT_RESOURCE)
roleBitmap
uint256
Bitmap of roles to grant (can include multiple roles via bitwise OR)
account
address
The address to grant roles to
returns
bool
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).

Role Information

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

Build docs developers (and LLMs) love