In ENS v2, each name can have its own dedicated registry contract that manages its direct subdomains. This creates a tree structure that mirrors the DNS hierarchy, enabling unprecedented flexibility in ownership models and access control.
Core Principle: A registry is responsible for exactly one name and all of its direct children. It has no knowledge of or control over its grandchildren or deeper descendants.
// The .eth registryIRegistry ethRegistry = rootRegistry.getSubregistry("eth");// It can register vitalik.ethethRegistry.register("vitalik", owner, subregistry, resolver, roles, expiry);// But it has NO control over sub.vitalik.eth// That's controlled by vitalik.eth's subregistry
Registries maintain a reference to their canonical parent:
// The subregistry should know its parent locationsubRegistry.setParent(parentRegistry, "sub");// Now it can report its canonical name(IRegistry parent, string memory label) = subRegistry.getParent();// parent = parentRegistry// label = "sub"
This enables:
Canonical name construction
Verification that a registry is properly configured
Create unlimited subdomain depth by chaining registries:
// Level 1: example.eth has a registryIRegistry level1 = new PermissionedRegistry(...);ethRegistry.register("example", owner1, level1, resolver1, roles1, expiry1);// Level 2: sub.example.eth has a registryIRegistry level2 = new PermissionedRegistry(...);level1.register("sub", owner2, level2, resolver2, roles2, expiry2);// Level 3: deep.sub.example.eth has a registryIRegistry level3 = new PermissionedRegistry(...);level2.register("deep", owner3, level3, resolver3, roles3, expiry3);// And so on...
Each level can use a different registry implementation:
// .eth uses PermissionedRegistry with expiryPermissionedRegistry ethRegistry = new PermissionedRegistry(...);// vitalik.eth uses a custom registry with different rulesCustomRegistry customRegistry = new CustomRegistry(...);ethRegistry.register("vitalik", owner, customRegistry, resolver, roles, expiry);// As long as it implements IRegistry, it works!
If a name has a resolver but no subregistry, the resolver can handle all subdomains:
// Register example.eth with resolver but NO subregistryethRegistry.register( "example", owner, IRegistry(address(0)), // No subregistry wildcardResolver, // This resolver handles *.example.eth roles, expiry);// Now ANY subdomain of example.eth will use wildcardResolver// - sub.example.eth → wildcardResolver// - www.example.eth → wildcardResolver// - anything.example.eth → wildcardResolver
The UniversalResolverV2 provides helper functions for registry discovery:
// Get the registry for a name (if it's canonical)IRegistry registry = universalResolver.findCanonicalRegistry( dnsEncode("vitalik.eth"));// Returns null if:// - Registry doesn't exist// - Registry's parent doesn't point to it// - Registry's getParent() doesn't match
IRegistry sub = parentRegistry.getSubregistry("exists");// sub == IRegistry(address(0))address resolver = parentRegistry.getResolver("exists");// resolver != address(0)// Name exists but has no subregistry (leaf node)
// Name was registered but expiredIRegistry sub = parentRegistry.getSubregistry("expired");// sub == IRegistry(address(0)) (treated as non-existent)uint64 expiry = parentRegistry.getExpiry(tokenId);// expiry < block.timestamp
Create subdomains that the parent owner cannot control:
1
Create Restricted Subregistry
Deploy a registry where the owner has no root-level roles:
PermissionedRegistry emancipatedRegistry = new PermissionedRegistry( hcaFactory, metadata, subdomainOwner, 0 // No root-level admin roles!);
2
Lock the Subregistry Pointer
Set the subregistry and revoke the ability to change it:
// Set the subregistryparentRegistry.setSubregistry(tokenId, emancipatedRegistry);// Revoke the role to change itparentRegistry.revokeRoles( tokenId, ROLE_SET_SUBREGISTRY, parentOwner);
3
Result: True Emancipation
The parent owner now cannot:
Change the subregistry pointer
Interfere with subdomain registrations
Modify subdomain permissions
But they still own the parent name itself.
Careful! Emancipated names are permanent. Once you revoke ROLE_SET_SUBREGISTRY, you cannot undo it unless you also retained that role at the root level.
// Original registryIRegistry oldRegistry = parentRegistry.getSubregistry("sub");// Parent changes the pointerparentRegistry.setSubregistry(tokenId, newRegistry);// oldRegistry is now non-canonical// It still exists but is disconnected from the hierarchy
Multiple names can share the same registry implementation:
// Deploy one registry contractPermissionedRegistry sharedRegistry = new PermissionedRegistry(...);// Use it for multiple namesethRegistry.register("alice", owner1, sharedRegistry, resolver1, roles1, expiry1);ethRegistry.register("bob", owner2, sharedRegistry, resolver2, roles2, expiry2);// ⚠️ WARNING: This is usually NOT what you want!// Both names would share the same subdomain namespace
// Initially register without subregistryregistry.register("name", owner, IRegistry(address(0)), resolver, roles, expiry);// Later, if subdomains are needed:IRegistry newSubregistry = new PermissionedRegistry(...);registry.setSubregistry(tokenId, newSubregistry);