How permissions inherit from ROOT_RESOURCE to specific names in ENS v2
One of the most powerful features of EnhancedAccessControl is permission inheritance from the ROOT_RESOURCE. This enables hierarchical permission structures where global administrators coexist with name-specific operators.
// Grant Alice global resolver management permissionregistry.grantRootRoles( RegistryRolesLib.ROLE_SET_RESOLVER, alice);// Alice can now set resolver for ANY nameuint256 tokenId1 = registry.getTokenId("example");uint256 tokenId2 = registry.getTokenId("another");registry.setResolver(tokenId1, resolver1); // ✓ Allowed via ROOT_RESOURCEregistry.setResolver(tokenId2, resolver2); // ✓ Allowed via ROOT_RESOURCE// Check permissionsregistry.hasRoles(tokenId1, ROLE_SET_RESOLVER, alice); // trueregistry.hasRoles(tokenId2, ROLE_SET_RESOLVER, alice); // trueregistry.hasRootRoles(ROLE_SET_RESOLVER, alice); // true
// Bob has global renewal permissionregistry.grantRootRoles( RegistryRolesLib.ROLE_RENEW, bob);// Bob also gets specific resolver permission for one nameuint256 exampleTokenId = registry.getTokenId("example");registry.grantRoles( exampleTokenId, RegistryRolesLib.ROLE_SET_RESOLVER, bob);// Bob's effective permissions:// For "example": ROLE_RENEW (inherited) + ROLE_SET_RESOLVER (specific)// For other names: ROLE_RENEW only (inherited)registry.renew(exampleTokenId, newExpiry); // ✓ Allowedregistry.setResolver(exampleTokenId, resolver); // ✓ Alloweduint256 anotherTokenId = registry.getTokenId("another");registry.renew(anotherTokenId, newExpiry); // ✓ Allowed (ROOT)registry.setResolver(anotherTokenId, resolver); // ✗ Denied (no role)
contract RenewalService { IPermissionedRegistry public registry; constructor(IPermissionedRegistry _registry) { registry = _registry; } function renewName(uint256 tokenId, uint64 duration) external { // This contract needs ROLE_RENEW at ROOT_RESOURCE // to renew any name users request uint64 currentExpiry = registry.getExpiry(tokenId); registry.renew(tokenId, currentExpiry + duration); }}// Deploy and authorize serviceRenewalService service = new RenewalService(registry);registry.grantRootRoles( RegistryRolesLib.ROLE_RENEW, address(service));// Service can now renew any name on behalf of usersservice.renewName(tokenId, 365 days);
// Delegate resolver management for your nameregistry.grantRoles( myTokenId, ROLE_SET_RESOLVER, dappOperator);// Allow friend to renew your nameregistry.grantRoles( myTokenId, ROLE_RENEW, friend);
There is no “precedence” in the traditional sense. Instead:
Additive Model: ROOT_RESOURCE and resource-specific roles are additive. Having a role in either place grants the permission.
You cannot “revoke” a ROOT_RESOURCE role with a resource-specific setting:
// Alice has ROLE_RENEW at ROOT_RESOURCEregistry.grantRootRoles(ROLE_RENEW, alice);// Revoking at resource level does NOT remove ROOT permissionregistry.revokeRoles(tokenId, ROLE_RENEW, alice);// Alice STILL has permission via ROOT_RESOURCEregistry.hasRoles(tokenId, ROLE_RENEW, alice); // true
To fully revoke:
// Must revoke at ROOT_RESOURCE levelregistry.revokeRootRoles(ROLE_RENEW, alice);// Now Alice has no permissionregistry.hasRoles(tokenId, ROLE_RENEW, alice); // false
// Grant admin role at ROOT_RESOURCE (via internal function)_grantRoles( ROOT_RESOURCE, ROLE_SET_RESOLVER_ADMIN, superAdmin, false);// superAdmin can now grant ROLE_SET_RESOLVER for ANY resourceregistry.grantRoles( tokenId1, ROLE_SET_RESOLVER, operator1);registry.grantRoles( tokenId2, ROLE_SET_RESOLVER, operator2);
In registry contracts, admin roles cannot be granted via grantRoles/grantRootRoles. They’re only assigned during registration or via internal logic.
// hasRoles: Checks ROOT_RESOURCE OR specific resourcefunction hasRoles( uint256 resource, uint256 roleBitmap, address account) public view returns (bool);// hasRootRoles: Checks ONLY ROOT_RESOURCEfunction hasRootRoles( uint256 roleBitmap, address account) public view returns (bool);
Example:
// Alice has ROLE_RENEW at ROOT_RESOURCEregistry.grantRootRoles(ROLE_RENEW, alice);// Bob has ROLE_RENEW for specific token onlyregistry.grantRoles(tokenId, ROLE_RENEW, bob);// Check methods return different resultsregistry.hasRoles(tokenId, ROLE_RENEW, alice); // true (via ROOT)registry.hasRootRoles(ROLE_RENEW, alice); // trueregistry.hasRoles(tokenId, ROLE_RENEW, bob); // true (specific)registry.hasRootRoles(ROLE_RENEW, bob); // false (not at root)registry.hasRoles(otherTokenId, ROLE_RENEW, alice); // true (via ROOT)registry.hasRoles(otherTokenId, ROLE_RENEW, bob); // false (only has for tokenId)
// Grant root role once for 100 namesregistry.grantRootRoles(ROLE_SET_RESOLVER, operator);// Cost: ~50k gas// vs. Grant individually for 100 namesfor (uint i = 0; i < 100; i++) { registry.grantRoles(tokenIds[i], ROLE_SET_RESOLVER, operator);}// Cost: ~5M gas
50x Cheaper: Granting a root role is approximately 50x more gas-efficient than granting the same role individually for 100 names.
Exercise Caution: ROOT_RESOURCE grants are extremely powerful. An account with ROLE_SET_RESOLVER at the root level can set the resolver for every name in the registry, including those it doesn’t own.
Best practices:
Limit root role holders to only essential accounts
// Good: Separate root roles by functionregistry.grantRootRoles(ROLE_RENEW, renewalService);registry.grantRootRoles(ROLE_SET_RESOLVER, migrationTool);// Avoid: Granting multiple powerful root roles to one addressregistry.grantRootRoles( ROLE_RENEW | ROLE_SET_RESOLVER | ROLE_SET_SUBREGISTRY | ROLE_UNREGISTER, singleAdmin // Too much power in one address);
// Get all root roles for an accountuint256 rootRoles = registry.roles(ROOT_RESOURCE, account);// Check specific root rolebool hasRootRenew = registry.hasRootRoles( RegistryRolesLib.ROLE_RENEW, account);// Get assignee count for root roles(uint256 counts, uint256 mask) = registry.getAssigneeCount( ROOT_RESOURCE, RegistryRolesLib.ROLE_SET_RESOLVER);
contract NameManagementService { IPermissionedRegistry public immutable registry; constructor(IPermissionedRegistry _registry) { registry = _registry; // Service needs root roles to operate on user names } function updateResolver( uint256 tokenId, address newResolver ) external { address owner = registry.ownerOf(tokenId); require(msg.sender == owner, "Not owner"); // Service can set resolver via ROOT_RESOURCE permission registry.setResolver(tokenId, newResolver); }}// Authorize service with root permissionsregistry.grantRootRoles( ROLE_SET_RESOLVER, address(service));