Name resolution in ENS v2 is fundamentally different from v1. Instead of a single lookup in a flat registry, resolution involves recursive traversal through a hierarchy of registry contracts, searching for the deepest resolver that can handle the query.
Core Principle: Resolution walks down the registry tree from root to leaf, remembering the last resolver it found. If no exact resolver exists for a name, its parent’s resolver may handle it (wildcard resolution).
Query: "What is the address for sub.vitalik.eth?"1. Start at Root Registry ↓2. Traverse to .eth Registry (remember: no resolver set) ↓3. Traverse to vitalik.eth (remember: resolver = 0x123...) ↓4. Look for sub.vitalik.eth (no registry exists) ↓5. Use vitalik.eth's resolver (0x123...) ↓6. Query resolver for sub.vitalik.eth's address ↓7. Return result
The core resolution logic is implemented in LibRegistry.findResolver:
function findResolver( IRegistry rootRegistry, bytes memory name, uint256 offset) internal view returns ( IRegistry exactRegistry, address resolver, bytes32 node, uint256 resolverOffset) { // Read the next label from the DNS-encoded name (bytes32 labelHash, uint256 next) = NameCoder.readLabel(name, offset); // Base case: reached the end (root) if (labelHash == bytes32(0)) { return (rootRegistry, address(0), bytes32(0), offset); } // Recursive case: process parent first (exactRegistry, resolver, node, resolverOffset) = findResolver( rootRegistry, name, next // Continue with next label ); // If we found a registry for the parent if (address(exactRegistry) != address(0)) { (string memory label, ) = NameCoder.extractLabel(name, offset); // Check if this level has a resolver address res = exactRegistry.getResolver(label); if (res != address(0)) { resolver = res; // Remember this resolver resolverOffset = offset; // Remember where we found it } // Try to go deeper exactRegistry = exactRegistry.getSubregistry(label); } // Update namehash for this level node = NameCoder.namehash(node, labelHash);}
// Setup: vitalik.eth has a resolver but sub.vitalik.eth does not existvitalikRegistry.getResolver("") // Returns: 0x123... (set on vitalik.eth)// When resolving sub.vitalik.eth:// 1. Traverse to vitalikRegistry// 2. Look for resolver on "sub" → not found// 3. Look for subregistry for "sub" → not found// 4. Fall back to vitalik.eth's resolver (0x123...)// 5. Resolver 0x123... handles the query for sub.vitalik.eth
// Setup: ipfs.eth has a wildcard resolverethRegistry.register( "ipfs", owner, IRegistry(address(0)), // No subregistry! ipfsGatewayResolver, // Handles all *.ipfs.eth roles, expiry);// Now ANY subdomain works:// - {cid}.ipfs.eth → ipfsGatewayResolver// - another.ipfs.eth → ipfsGatewayResolver// - deeply.nested.ipfs.eth → ipfsGatewayResolver
App Subdomain Router
// Setup: myapp.eth routes all subdomains to an app resolverethRegistry.register( "myapp", owner, IRegistry(address(0)), appSubdomainResolver, // Handles user.myapp.eth, api.myapp.eth, etc. roles, expiry);// The resolver can:// - Parse the subdomain from the query// - Route to different backends// - Generate responses dynamically
Organization Hierarchy
// Setup: company.eth has departmentscompanyRegistry.register( "engineering", deptOwner, IRegistry(address(0)), deptResolver, // Handles *.engineering.company.eth roles, expiry);// Allows:// - alice.engineering.company.eth// - bob.engineering.company.eth// Without registering each name individually
// Create a subregistry even if emptyIRegistry subRegistry = new PermissionedRegistry(...);parentRegistry.register( "sub", owner, subRegistry, // Subregistry exists specificResolver, // This exact resolver will be used roles, expiry);// Now sub.parent.eth uses specificResolver// NOT the parent's resolver
function findResolver(bytes memory name) public view returns (address resolver, bytes32 node, uint256 offset) { // Use LibRegistry to traverse the hierarchy (, resolver, node, offset) = LibRegistry.findResolver( ROOT_REGISTRY, name, 0 // Start at beginning of name );}
ENS v2 supports CCIP-Read (EIP-3668) for off-chain resolution:
// If resolver reverts with OffchainLookup:try resolver.resolve(node, data) returns (bytes memory result) { return result;} catch (bytes memory error) { // Check if it's CCIP-Read revert if (bytes4(error) == OffchainLookup.selector) { // Client should make off-chain request // Then call back with proof }}
CCIP-Read enables resolvers to fetch data from off-chain sources (L2s, databases, etc.) while maintaining security through cryptographic proofs.
// Name exists but no resolver set anywhere in the path(, address resolver, ,) = universalResolver.findResolver(name);// resolver == address(0)// Solution: Set a resolver at some level of the hierarchyregistry.setResolver(tokenId, resolverAddress);
Name Expired
// Registry returns address(0) for expired namesIRegistry registry = parentRegistry.getSubregistry("expired");// registry == address(0)// Resolution treats this as non-existent// Solution: Renew the nameparentRegistry.renew(tokenId, newExpiry);
Registry Path Broken
// Parent's subregistry pointer was changedIRegistry old = parentRegistry.getSubregistry("name");parentRegistry.setSubregistry(tokenId, newRegistry);// Old registry is now disconnected from canonical path// Resolution won't find it
Invalid DNS Encoding
// Malformed DNS-encoded namebytes memory invalid = hex"ff..."; // Invalid label length// Will revert during NameCoder.readLabelrevert DNSDecodingFailed(invalid);
// Set up wildcard resolutionfunction enableWildcardSubdomains(uint256 tokenId, address wildcardResolver) external { // Don't create a subregistry registry.setSubregistry(tokenId, IRegistry(address(0))); // Set the wildcard resolver registry.setResolver(tokenId, wildcardResolver); // Now all *.yourname.eth use wildcardResolver}
// ✅ GOOD: Set resolver on the name itselfregistry.setResolver(tokenId, resolver);// ❌ BAD: Relying on parent's wildcard when you have a subregistry// (Won't work - exact match takes precedence)
Handle Missing Resolvers Gracefully
const { resolver } = await universalResolver.findResolver(name);if (resolver === ethers.constants.AddressZero) { // Resolver not set - handle appropriately return null; // or throw, or use default}
Validate Canonical Names
// Ensure a registry is in the canonical pathbytes memory canonicalName = universalResolver.findCanonicalName(registry);require(canonicalName.length > 0, "Not canonical");
Monitor Resolution Events
While there are no direct resolution events, monitor: