Universal Resolver V2
The Universal Resolver V2 is the primary public entry point for ENS name resolution. It resolves names by traversing the hierarchical registry tree, walking from the root registry down through subregistries to locate the correct resolver for any name. It also provides functions for navigating and verifying the registry hierarchy itself.
Architecture
UniversalResolverV2 extends AbstractUniversalResolver, a shared base contract that implements all resolution logic: forward resolution, reverse resolution, CCIP-Read gateway batching, and callback chaining. The only function the base leaves abstract is findResolver(), which each version overrides with its own registry traversal strategy:
- URv1 walks a flat ENS registry
- URv2 walks the hierarchical registry tree via
getSubregistry()at each level
Because the walk passes through each parent registry, if a parent name expires or its subregistry is removed, the registry returns address(0) and all subnames stop resolving automatically.
resolve(), reverse(), and all CCIP-Read infrastructure work identically across both versions.
Resolution
The Universal Resolver resolves names by walking down the registry tree from the root, looking for the deepest resolver along the path. At each level, it calls getResolver(label) on the current registry. If a resolver exists, it's remembered. Then it calls getSubregistry(label) to descend to the next level. The resolver that covers the longest matching suffix of the name wins.
resolve() and reverse() both call findResolver internally via requireResolver, which reverts if no suitable resolver is found.
Using the structure from the registry hierarchy diagrams, here are two examples showing how findResolver() walks the tree:
Example 1
Resolving inigo.montoya.eth: the walk descends from root, checking for resolvers at each level:
Resolver 1 is found at the .eth level, but Resolver 2 is found one level deeper at montoya.eth. Resolver 2 wins because it covers the longest matching suffix, in this case the full name inigo.montoya.eth.
Example 2
Resolving domingo.montoya.eth: the walk follows the same path, but domingo has no resolver:
Resolver 1 is found at the .eth level, but domingo has no resolver set. Resolver 1 wins as the longest-suffix match, covering montoya.eth. Any subname of montoya.eth without its own resolver will fall back to Resolver 1 the same way.
The Algorithm
findResolver() implements this longest-suffix match. It recursively descends from the root, and at each level:
- Calls
getResolver(label). If non-zero, overwrites the previously remembered resolver. - Calls
getSubregistry(label). If non-zero, continues descending into the subregistry.
The final remembered resolver is the one used for the actual record query.
Registry Navigation
ENSv2 provides functions for locating registries within the hierarchy and verifying their position. All functions described here are exposed on the UniversalResolverV2 contract.
All navigation functions walk down from the root registry except findCanonicalName, which walks up via getParent(), and findCanonicalRegistry, which does both.
findExactRegistry
Walks top-down from root, calling getSubregistry() at each label. Returns the subregistry that the target name points to, or address(0) if any link in the chain is missing.
For nick.eth:
root
└── getSubregistry("eth") → .eth registry
└── getSubregistry("nick") → nick.eth subregistry ← returnedThis is the registry where nick.eth's subnames live. If nick.eth has no subregistry set, the final step returns address(0).
Use findExactRegistry when you need to interact with a specific registry and trust the hierarchy (e.g., checking roles or reading name state). For security-sensitive contexts, use findCanonicalRegistry instead.
findParentRegistry
Walks top-down to find the registry that contains a name's entry, rather than the subregistry the name points to. It strips the first label and calls findExactRegistry on the parent suffix.
For nick.eth:
root
└── getSubregistry("eth") → .eth registry ← returnedThe .eth registry is where nick is an entry. Contrast with findExactRegistry("nick.eth"), which returns nick.eth's own subregistry (one level deeper).
This is used internally by findOwner to locate the registry that holds ownership information for a name.
findRegistries
Walks top-down and builds the complete ancestry array for a name, from innermost to outermost. Each position in the array corresponds to one label in the name, with the root registry appended at the end.
findRegistries("") → [<root>]
findRegistries("eth") → [<eth>, <root>]
findRegistries("nick.eth") → [<nick>, <eth>, <root>]
findRegistries("sub.nick.eth") → [address(0), <nick>, <eth>, <root>]If a name has no subregistry at some level, that position in the array is address(0). In the last example, sub.nick.eth has no subregistry, so the first element is zero, but its parent registries are all present.
findCanonicalName
Because registries have no inherent concept of "their name" (a registry can be mounted at multiple positions via namespace aliasing), a canonical name only exists when both directions of the hierarchy agree: setParent() establishes the backward pointer, and findCanonicalName verifies at each step that the parent's forward pointer (getSubregistry) points back to the same registry.
For the nick.eth subregistry (walking upward):
nick.eth subregistry
├── getParent() → (.eth registry, "nick")
│ └── verify: eth.getSubregistry("nick") == nick.eth subregistry ✓
└── .eth registry
├── getParent() → (root, "eth")
│ └── verify: root.getSubregistry("eth") == .eth registry ✓
└── root reached → canonical name: "nick.eth"Returns empty bytes if any link is broken: if a registry has no parent set, or if parent.getSubregistry(label) points to a different address.
findCanonicalRegistry
findCanonicalRegistry verifies this by combining both directions:
For nick.eth:
Down: findExactRegistry("nick.eth")
└── root.getSubregistry("eth") → .eth registry
└── eth.getSubregistry("nick") → 0xABCD
Up: findCanonicalName(0xABCD)
├── 0xABCD.getParent() → (.eth registry, "nick")
│ └── verify: eth.getSubregistry("nick") == 0xABCD ✓
└── reconstructed name: "nick.eth" == input name ✓ → return 0xABCDThe bidirectional check prevents aliasing attacks: a registry could be mounted at one position in the tree but claim (via getParent()) to be at another. This is the function to use when verifying a registry's legitimacy, for example in marketplaces or any context where a name is being purchased or trusted.
findOwner
Finds the current owner of a name. IOwnedRegistry is a minimal interface that extends IRegistry with a single findOwner(label) function, returning the owner of a label. PermissionedRegistry implements it, but any custom registry with ownership can too.
For nick.eth:
findParentRegistry("nick.eth") → .eth registry
├── supports IOwnedRegistry? (ERC-165 check) ✓
└── .eth.findOwner("nick") → 0x1234 ← returnedReturns address(0) if the parent registry doesn't exist, doesn't implement IOwnedRegistry, or the label has no owner.
Reference
View Functions
findResolver(name) | Find the resolver for a name by walking down the registry hierarchy. |
findExactRegistry(name) | Find the registry at a name's position by walking down from root. |
findParentRegistry(name) | Find the parent registry for a name (the registry containing the name's entry). |
findRegistries(name) | Return all registries in a name's ancestry, innermost first. |
findCanonicalName(registry) | Reconstruct a registry's DNS-encoded name by walking up via getParent(). |
findCanonicalRegistry(name) | Find the registry for a name, verified canonical via bidirectional walk. |
findOwner(name) | Find the current owner of a name. |
resolve(name, data) | Forward-resolve a single record for a DNS-encoded name. For batch resolution, encode data as multicall(bytes[]). |
resolveWithGateways(name, data, gateways) | Same as resolve, but with custom CCIP-Read gateway URLs. |
resolveWithResolver(resolver, name, data, gateways) | Resolve using a specific resolver address, bypassing the findResolver lookup. |
reverse(lookupAddress, coinType) | Reverse resolution per ENSIP-19: look up the primary name for an address, then verify via forward resolution. |
reverseWithGateways(lookupAddress, coinType, gateways) | Same as reverse, but with custom CCIP-Read gateway URLs. |
requireResolver(name) | Same as findResolver, but reverts if no suitable resolver is found. Reverts with ResolverNotFound if: no resolver exists, or the resolver does not support IExtendedResolver and was found at a parent suffix (non-zero offset). Reverts with ResolverNotContract if the resolver was matched exactly (zero offset), does not support IExtendedResolver, and has no deployed code. Used internally by resolve() and reverse(). |
Constants
ROOT_REGISTRY | The ENSv2 root registry. All hierarchy traversal starts from this address. Set once at deployment (immutable). |
batchGatewayProvider | Default gateway provider for CCIP-Read batching. Set once at deployment (immutable). |
Errors
ResolverNotFound(name) | No resolver found for the name. |
ResolverNotContract(name, resolver) | Resolver address has no deployed code. |
UnsupportedResolverProfile(selector) | Resolver doesn't support the requested function. |
ResolverError(errorData) | Resolver reverted during resolution. |
ReverseAddressMismatch(primary, primaryAddress) | Forward resolution of the primary name doesn't match the original address. |
HttpError(status, message) | HTTP error from a CCIP-Read gateway. |