Skip to main content

Overview

PermissionedRegistry is the foundational contract for ENS v2’s registry system. It combines ERC-1155 token ownership with fine-grained role-based access control to manage domain names as tradeable, permissioned assets.

Contract Details

Location: contracts/src/registry/PermissionedRegistry.sol Inherits:
  • IRegistry - Core registry interface
  • ERC1155Singleton - Single-token-per-ID implementation
  • EnhancedAccessControl - Role-based permission system
  • IPermissionedRegistry - Extended registry interface
  • MetadataMixin - Token URI metadata support
Interface Selector: 0xafff3a63

Architecture

State Diagram

The registry implements a three-state lifecycle for all names:
                     register()
                  +ROLE_REGISTRAR
      +------------------->----------------------+
      |                                          |
      |                renew()                   |    renew()
      |              +ROLE_RENEW                 |  +ROLE_RENEW
      |               +------+                   |   +------+
      |               |      |                   |   |      |
      ʌ               ʌ      v                   v   v      |
  AVAILABLE --------> RESERVED -------------> REGISTERED >--+
      ʌ    register()    v       register()        v
      |    w/owner=0     | +ROLE_REGISTER_RESERVED |
      | +ROLE_REGISTRAR  |                         |
      |                  |                         |
      +--------<---------+------------<------------+
                    unregister()
                 +ROLE_UNREGISTER

Storage Structure

Entry Struct

Each name is stored as a packed Entry struct:
struct Entry {
    uint32 eacVersionId;      // Enhanced Access Control version
    uint32 tokenVersionId;    // ERC-1155 token version
    IRegistry subregistry;    // Optional child registry
    uint64 expiry;            // Unix timestamp expiration
    address resolver;         // Resolver contract address
}
All fields fit into a single 256-bit storage slot, minimizing gas costs.

Internal Storage

mapping(uint256 storageId => Entry entry) internal _entries;
IRegistry internal _parent;
string internal _childLabel;
The storageId is the label ID with version bits cleared (version 0).

Constructor

constructor(
    IHCAFactoryBasic hcaFactory,
    IRegistryMetadata metadata,
    address ownerAddress,
    uint256 ownerRoles
)
Parameters:
  • hcaFactory - Factory for hierarchical contract addresses
  • metadata - Metadata provider for token URIs
  • ownerAddress - Initial owner to receive roles (can be address(0))
  • ownerRoles - Initial role bitmap to grant owner
Example:
PermissionedRegistry registry = new PermissionedRegistry(
    hcaFactory,
    metadataProvider,
    msg.sender,
    RegistryRolesLib.ROLE_REGISTRAR |
    RegistryRolesLib.ROLE_REGISTRAR_ADMIN
);

Core Functions

register

function register(
    string memory label,
    address owner,
    IRegistry registry,
    address resolver,
    uint256 roleBitmap,
    uint64 expiry
) public virtual returns (uint256 tokenId)
Registers or reserves a new name, or completes a reservation. Parameters:
  • label - The DNS label (1-255 bytes)
  • owner - Recipient address (address(0) for reservation)
  • registry - Optional subregistry contract
  • resolver - Optional resolver address
  • roleBitmap - Roles to grant to owner (must be 0 if owner is 0)
  • expiry - Expiration timestamp (0 to reuse existing for reserved names)
Returns:
  • tokenId - The minted token ID (or next token ID for reservations)
State Transitions:
Requires: ROLE_REGISTRAR on ROOT_RESOURCE
uint256 tokenId = registry.register(
    "newname",
    msg.sender,
    IRegistry(address(0)),
    address(resolver),
    RegistryRolesLib.ROLE_SET_RESOLVER,
    uint64(block.timestamp + 365 days)
);
// Emits: NameRegistered
// Token minted to msg.sender
Events:
// For registrations
event NameRegistered(
    uint256 indexed tokenId,
    bytes32 indexed labelHash,
    string label,
    address owner,
    uint64 expiry,
    address indexed sender
);

// For reservations
event NameReserved(
    uint256 indexed tokenId,
    bytes32 indexed labelHash,
    string label,
    uint64 expiry,
    address indexed sender
);
Errors:
error NameAlreadyRegistered(string label);
error NameAlreadyReserved(string label);
error CannotSetPastExpiration(uint64 expiry);
error EACCannotGrantRoles(uint256 resource, uint256 roleBitmap, address sender);

unregister

function unregister(uint256 anyId) public virtual
Removes a registered or reserved name, making it available again. Requires: ROLE_UNREGISTER on the name’s resource Parameters:
  • anyId - Label ID, token ID, or resource ID
Behavior:
  • Burns the token if one exists
  • Increments both version IDs
  • Sets expiry to current timestamp
  • Does not clear subregistry or resolver
Example:
// Must have ROLE_UNREGISTER
registry.unregister(tokenId);
// Name becomes AVAILABLE
// Can be re-registered immediately
Events:
event NameUnregistered(uint256 indexed tokenId, address indexed sender);

renew

function renew(uint256 anyId, uint64 newExpiry) public override
Extends the expiration of a registered or reserved name. Requires: ROLE_RENEW on the name’s resource Parameters:
  • anyId - Label ID, token ID, or resource ID
  • newExpiry - New expiration timestamp (must be greater than current)
Example:
// Extend by one year
uint64 currentExpiry = registry.getExpiry(tokenId);
registry.renew(tokenId, currentExpiry + 365 days);
Events:
event ExpiryUpdated(
    uint256 indexed tokenId,
    uint64 newExpiry,
    address indexed sender
);
Errors:
error CannotReduceExpiration(uint64 oldExpiration, uint64 newExpiration);
error NameExpired(uint256 tokenId);

setSubregistry

function setSubregistry(uint256 anyId, IRegistry registry) public virtual
Sets the subregistry for a registered name. Requires: ROLE_SET_SUBREGISTRY on the name’s resource Parameters:
  • anyId - Label ID, token ID, or resource ID
  • registry - New subregistry contract (or address(0) to clear)
Example:
// Deploy subregistry for "dao"
IRegistry daoRegistry = deployUserRegistry(...);

// Attach it to the name
registry.setSubregistry(daoTokenId, daoRegistry);

// Now dao.eth can manage its subdomains
Events:
event SubregistryUpdated(
    uint256 indexed tokenId,
    IRegistry subregistry,
    address indexed sender
);

setResolver

function setResolver(uint256 anyId, address resolver) public virtual
Sets the resolver for a registered name. Requires: ROLE_SET_RESOLVER on the name’s resource Parameters:
  • anyId - Label ID, token ID, or resource ID
  • resolver - New resolver address
Example:
registry.setResolver(tokenId, address(newResolver));
Events:
event ResolverUpdated(
    uint256 indexed tokenId,
    address resolver,
    address indexed sender
);

setParent

function setParent(
    IRegistry parent,
    string memory label
) public virtual
Sets the canonical parent registry and label. Requires: ROLE_SET_PARENT on ROOT_RESOURCE Parameters:
  • parent - Parent registry contract
  • label - This registry’s label in the parent
Example:
// Set parent reference for dao.eth registry
registry.setParent(ethRegistry, "dao");
Events:
event ParentUpdated(
    IRegistry indexed parent,
    string label,
    address indexed sender
);

Access Control Integration

Role Management

PermissionedRegistry overloads Enhanced Access Control methods to work with anyId:
// Grant roles using any identifier type
registry.grantRoles(
    anyId,  // Label ID, token ID, or resource ID
    RegistryRolesLib.ROLE_SET_RESOLVER,
    delegate
);

// Revoke roles
registry.revokeRoles(
    anyId,
    RegistryRolesLib.ROLE_SET_RESOLVER,
    delegate
);

// Check roles
bool hasRole = registry.hasRoles(
    anyId,
    RegistryRolesLib.ROLE_SET_RESOLVER,
    account
);

// Get all roles for an account
uint256 roles = registry.roles(anyId, account);

Admin Role Restrictions

The registry restricts admin role management to prevent privilege escalation:
function _getSettableRoles(
    uint256 resource,
    address account
) internal view virtual override returns (uint256) {
    uint256 allRoles = super.roles(resource, account) |
                       super.roles(ROOT_RESOURCE, account);
    uint256 adminRoleBitmap = allRoles & EACBaseRolesLib.ADMIN_ROLES;
    return adminRoleBitmap >> 128;  // Only non-admin roles
}
Admin roles can only be granted during initial registration, not via grantRoles(). This prevents unauthorized privilege escalation.

View Functions

getSubregistry

function getSubregistry(string calldata label)
    public view virtual returns (IRegistry)
Returns the subregistry for a label, or address(0) if expired or unregistered.

getResolver

function getResolver(string calldata label)
    public view virtual returns (address)
Returns the resolver for a label, or address(0) if expired or unregistered.

getParent

function getParent()
    public view virtual returns (IRegistry parent, string memory label)
Returns the parent registry and this registry’s label in the parent.

getExpiry

function getExpiry(uint256 anyId) public view returns (uint64)
Returns the expiration timestamp for a name.

getResource

function getResource(uint256 anyId) public view returns (uint256)
Converts any identifier to the current resource ID for access control.

getTokenId

function getTokenId(uint256 anyId) public view returns (uint256)
Converts any identifier to the current token ID.

getStatus

function getStatus(uint256 anyId) public view returns (Status)
Returns the current status: AVAILABLE, RESERVED, or REGISTERED.

getState

function getState(uint256 anyId) public view returns (State memory state)
Returns complete state information:
struct State {
    Status status;
    uint64 expiry;
    address latestOwner;
    uint256 tokenId;
    uint256 resource;
}
Example:
State memory state = registry.getState(labelId);

if (state.status == Status.REGISTERED) {
    console.log("Owner:", state.latestOwner);
    console.log("Expires:", state.expiry);
    console.log("Token ID:", state.tokenId);
}

latestOwnerOf

function latestOwnerOf(uint256 tokenId) public view returns (address)
Returns the owner of a token ID, even if expired. Does not check expiry.

ownerOf

function ownerOf(uint256 tokenId) public view returns (address)
Returns the owner only if the token is valid (current version and not expired).

Token Transfer Controls

The registry overrides _update to enforce transfer permissions:
function _update(
    address from,
    address to,
    uint256[] memory tokenIds,
    uint256[] memory values
) internal virtual override {
    bool externalTransfer = to != address(0) && from != address(0);
    if (externalTransfer) {
        // Check ROLE_CAN_TRANSFER_ADMIN for each token
        for (uint256 i; i < tokenIds.length; ++i) {
            if (!hasRoles(tokenIds[i], RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN, from)) {
                revert TransferDisallowed(tokenIds[i], from);
            }
        }
    }
    super._update(from, to, tokenIds, values);
    if (externalTransfer) {
        // Transfer all roles to new owner
        for (uint256 i; i < tokenIds.length; ++i) {
            _transferRoles(getResource(tokenIds[i]), from, to, false);
        }
    }
}
When a token is transferred, all roles associated with that resource are automatically transferred to the new owner.

Token Regeneration

When roles are granted or revoked, the token is regenerated:
function _regenerateToken(uint256 anyId) internal {
    Entry storage entry = _entry(anyId);
    if (!_isExpired(entry.expiry)) {
        uint256 tokenId = _constructTokenId(anyId, entry);
        address owner = super.ownerOf(tokenId);
        if (owner != address(0)) {
            _burn(owner, tokenId, 1);
            ++entry.tokenVersionId;
            uint256 newTokenId = _constructTokenId(tokenId, entry);
            _mint(owner, newTokenId, 1, "");
            emit TokenRegenerated(tokenId, newTokenId);
        }
    }
}
This ensures:
  • ERC-1155 compliance (immutable token IDs)
  • Unique token IDs for different permission sets
  • No balance can exceed 1

Interface Support

function supportsInterface(bytes4 interfaceId)
    public view virtual override returns (bool)
{
    return
        interfaceId == type(IPermissionedRegistry).interfaceId ||
        interfaceId == type(IStandardRegistry).interfaceId ||
        interfaceId == type(IRegistry).interfaceId ||
        super.supportsInterface(interfaceId);
}

Usage Patterns

Creating a Registry

// Deploy with initial admin
PermissionedRegistry registry = new PermissionedRegistry(
    hcaFactory,
    metadataProvider,
    admin,
    RegistryRolesLib.ROLE_REGISTRAR |
    RegistryRolesLib.ROLE_REGISTRAR_ADMIN |
    RegistryRolesLib.ROLE_SET_PARENT |
    RegistryRolesLib.ROLE_SET_PARENT_ADMIN
);

// Set parent reference
registry.setParent(parentRegistry, "myregistry");

Delegating Registration Rights

// Grant registrar role to controller
registry.grantRoles(
    ROOT_RESOURCE,
    RegistryRolesLib.ROLE_REGISTRAR,
    controller
);

// Controller can now register names

Managing Name Permissions

// Register with specific permissions
uint256 tokenId = registry.register(
    "subdao",
    daoContract,
    IRegistry(address(0)),
    address(resolver),
    RegistryRolesLib.ROLE_SET_RESOLVER |
    RegistryRolesLib.ROLE_RENEW,
    uint64(block.timestamp + 365 days)
);

// Grant additional permissions later
registry.grantRoles(
    tokenId,
    RegistryRolesLib.ROLE_SET_SUBREGISTRY,
    daoContract
);

Security Considerations

Admin Roles: Admin roles can only be granted during registration. This prevents privilege escalation attacks.
Transfer Locks: Names without ROLE_CAN_TRANSFER_ADMIN cannot be transferred, creating non-transferable names.
Expiry Checks: Always verify expiry before trusting returned data. Expired names return address(0) for most queries.

UserRegistry

Upgradeable extension of PermissionedRegistry

WrapperRegistry

Migration-enabled registry for v1 wrapped names

Registry Roles

Complete role definitions and permissions

Enhanced Access Control

Underlying permission system

Build docs developers (and LLMs) love