Complete reference for roles in ENS v2 registry contracts
Registry contracts use the EnhancedAccessControl system with a specific set of roles defined in RegistryRolesLib.sol. Each role controls specific operations on names within the registry.
Root only: Role must be granted at ROOT_RESOURCE (applies to all names)Root or Token: Role can be granted globally or per-nameToken only: Role can only be granted for specific names
Purpose: Register new subnamesBit Position: 0Admin: Bit 128Scope: Root onlyThe registrar role allows creating new subnames under the registry. This is a root-only role because the resource (name) doesn’t exist yet at registration time.
// Grant registrar permission to a controller contractregistry.grantRootRoles( RegistryRolesLib.ROLE_REGISTRAR, controllerAddress);
Example Use Case: ETH Registrar Controller uses this role to register .eth names.
Purpose: Register names that are in RESERVED statusBit Position: 4Admin: Bit 132Scope: Root onlyReserved names have an expiry but no owner. This role allows registering them before they expire.
// Reserve a name first (owner = address(0))registry.register( "premium", address(0), // No owner = RESERVED status IRegistry(address(0)), address(0), 0, futureExpiry);// Later, register the reserved name (requires ROLE_REGISTER_RESERVED)registry.register( "premium", actualOwner, registry, resolver, roles, 0 // Use existing expiry);
Example Use Case: Premium name auctions or phased releases.
Purpose: Change the parent registry and labelBit Position: 8Admin: Bit 136Scope: Root onlyControls the parent registry relationship for proper hierarchical resolution.
Purpose: Unregister (burn) namesBit Position: 12Admin: Bit 140Scope: Root or TokenAllows burning a name by setting its expiry to the current timestamp.
// Grant contract-wide unregister permissionregistry.grantRootRoles( RegistryRolesLib.ROLE_UNREGISTER, moderator);// Moderator can now unregister any nameregistry.unregister(tokenId);
Example Use Case: Content moderation or self-service name deletion.
Purpose: Extend name expiration datesBit Position: 16Admin: Bit 144Scope: Root or TokenAllows extending the expiry timestamp of a name.
// Grant renewal permission to subscription serviceregistry.grantRootRoles( RegistryRolesLib.ROLE_RENEW, subscriptionContract);// Subscription contract can renew any nameregistry.renew(tokenId, newExpiry);
Cannot Reduce Expiry: The renew function only allows extending expiry, not shortening it. Use unregister to expire a name immediately.
Example Use Case: Automated renewal services or gifting renewals.
Purpose: Change the subregistry contract addressBit Position: 20Admin: Bit 148Scope: Root or TokenControls which registry contract handles subnames under a given name.
// Grant global subregistry managementregistry.grantRootRoles( RegistryRolesLib.ROLE_SET_SUBREGISTRY, migrationController);// Migration controller can update any subregistryregistry.setSubregistry(tokenId, newSubregistry);
Example Use Case: Deploying custom subname registries or upgrading subdomain management.
Purpose: Change the resolver contract addressBit Position: 24Admin: Bit 152Scope: Root or TokenControls which resolver contract handles record lookups for a name.
// Grant resolver management to migration toolregistry.grantRootRoles( RegistryRolesLib.ROLE_SET_RESOLVER, migrationTool);// Migration tool can update any resolverregistry.setResolver(tokenId, newResolver);
Example Use Case: Delegating resolver management to dapps while retaining ownership.
Purpose: Control NFT transfer permissionsBit Position: 144 (admin role only, no corresponding regular role)Admin: 144Scope: Token onlyThis is a special admin-only role automatically granted to the name owner during registration. It controls whether the name can be transferred as an NFT.
ROLE_CAN_TRANSFER_ADMIN is unique because it has no corresponding regular role. It exists only as an admin role.
Automatic Assignment:
// During registration, owner automatically receives ROLE_CAN_TRANSFER_ADMINregistry.register( "alice", owner, registry, resolver, ROLE_SET_RESOLVER, // Regular role granted expiry);// owner now has ROLE_CAN_TRANSFER_ADMIN by default
Creating Soulbound Names:
// Revoke transfer permission to make name non-transferableregistry.revokeRoles( tokenId, RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN, owner // Revoking from yourself);// Name is now soulbound (cannot be transferred)// Attempting to transfer will revert with TransferDisallowed
Irreversible: Once you revoke ROLE_CAN_TRANSFER_ADMIN from yourself, you cannot grant it back. The name becomes permanently soulbound.
Transfer Check:
PermissionedRegistry.sol:386
if (!hasRoles(tokenIds[i], RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN, from)) { revert TransferDisallowed(tokenIds[i], from);}
// Check if user can set resolverbool canSetResolver = registry.hasRoles( tokenId, RegistryRolesLib.ROLE_SET_RESOLVER, user);if (canSetResolver) { // Proceed with resolver update}
// Grant operational permissions but not transfer rightsuint256 delegatedRoles = RegistryRolesLib.ROLE_SET_RESOLVER | RegistryRolesLib.ROLE_SET_SUBREGISTRY;registry.grantRoles(tokenId, delegatedRoles, delegate);// delegate can manage resolver and subregistry// but CANNOT transfer the name (no ROLE_CAN_TRANSFER_ADMIN)
// 1. Deploy subregistry with limited root rolesPermissionedRegistry subregistry = new PermissionedRegistry( hcaFactory, metadata, subOwner, 0 // No root roles for owner);// 2. Set as locked subregistry in parentparentRegistry.setSubregistry(tokenId, IRegistry(address(subregistry)));// Result: Parent owner cannot interfere with subnames// Even with root-level permissions in parent
// Grant contract-wide administrative permissionsuint256 adminRoles = RegistryRolesLib.ROLE_SET_RESOLVER | RegistryRolesLib.ROLE_SET_SUBREGISTRY | RegistryRolesLib.ROLE_RENEW | RegistryRolesLib.ROLE_UNREGISTER;registry.grantRootRoles(adminRoles, admin);// admin can now manage resolver, subregistry, renewal, and unregistration// for ALL names in the registry
Registry Restriction: Admin roles can only be granted during name registration in registry contracts. They cannot be granted afterward using grantRoles.
When a name is transferred, roles behave as follows:Admin Roles: Transfer to new ownerRegular Roles (granted to owner): Transfer to new ownerRegular Roles (granted to others): Remain intact
// Initial state: Alice owns name// Alice has: ROLE_SET_RESOLVER_ADMIN, ROLE_SET_RESOLVER// Bob has: ROLE_RENEW (granted by Alice)// Alice transfers to Charlieregistry.safeTransferFrom(alice, charlie, tokenId, 1, "");// Final state:// Charlie has: ROLE_SET_RESOLVER_ADMIN, ROLE_SET_RESOLVER// Bob still has: ROLE_RENEW// Alice has: nothing
This is implemented in /home/daytona/workspace/source/contracts/src/registry/PermissionedRegistry.sol:394:
if (externalTransfer) { for (uint256 i; i < tokenIds.length; ++i) { _transferRoles(getResource(tokenIds[i]), from, to, false); }}