Skip to main content

Overview

The UniversalResolverV2 contract extends the ENS universal resolution system to support multiple registries in the ENS v2 architecture. It provides a unified interface for resolving names across different registry namespaces while maintaining backward compatibility with the ENS v1 resolver interface.
UniversalResolverV2 inherits from AbstractUniversalResolver and uses the LibRegistry library for registry traversal and resolution.

Key Features

Multi-Registry Support

Resolve names across multiple connected registries in the hierarchy

Canonical Lookups

Find canonical names for registries and canonical registries for names

Registry Traversal

Walk the entire registry ancestry for any given name

CCIP-Read Compatible

Supports batch gateway providers for off-chain resolution

Contract Address

The contract is deployed with an immutable reference to the root registry:
IRegistry public immutable ROOT_REGISTRY;

Constructor

constructor(
    IRegistry root,
    IGatewayProvider batchGatewayProvider
)
root
IRegistry
The root registry contract address
batchGatewayProvider
IGatewayProvider
Gateway provider for CCIP-Read batch operations

Core Resolution

findResolver

Finds the resolver, node, and offset for a given name by traversing the registry hierarchy.
function findResolver(
    bytes memory name
) public view returns (
    address resolver,
    bytes32 node,
    uint256 offset
)
name
bytes
DNS-encoded name to resolve
resolver
address
The resolver address, or address(0) if not found
node
bytes32
The namehash of the resolved portion
offset
uint256
Offset into the name where the resolver was found

Example: Finding a Resolver

// Resolve "alice.eth"
bytes memory name = hex"05616c696365036574680000"; // DNS-encoded "alice.eth"

(address resolver, bytes32 node, uint256 offset) = universalResolver.findResolver(name);

if (resolver != address(0)) {
    // Found a resolver for alice.eth or eth
    // Use it to query records
}
The resolver returned might be for the exact name or for a parent in the hierarchy. The offset parameter indicates which portion of the name was actually resolved.

Canonical Name Operations

findCanonicalName

Constructs the canonical DNS-encoded name for a given registry address.
function findCanonicalName(
    IRegistry registry
) external view returns (bytes memory)
registry
IRegistry
The registry contract to name
name
bytes
DNS-encoded canonical name, or empty if not canonical

Example: Getting Canonical Name

// Get canonical name for a registry
IRegistry ethRegistry = IRegistry(0x123...);
bytes memory canonicalName = universalResolver.findCanonicalName(ethRegistry);

if (canonicalName.length > 0) {
    // Registry is canonical and has a name
    // canonicalName might be DNS-encoded "eth"
}
Returns an empty bytes array if the registry is not canonical or cannot be named from the root.

findCanonicalRegistry

Finds the canonical registry for a given DNS-encoded name.
function findCanonicalRegistry(
    bytes calldata name
) external view returns (IRegistry)
name
bytes
DNS-encoded name to look up
registry
IRegistry
Canonical registry address, or IRegistry(address(0)) if not canonical

Example: Verifying Canonical Registry

// Check if "alice.eth" points to a canonical registry
bytes memory name = hex"05616c696365036574680000";

IRegistry registry = universalResolver.findCanonicalRegistry(name);

if (address(registry) != address(0)) {
    // This is a canonical registry for alice.eth
    // The registry's canonical name equals "alice.eth"
}
A registry is canonical for a name if:
  1. The registry exists at that name path
  2. Walking backwards from the registry produces the same name

Registry Hierarchy Traversal

findRegistries

Finds all registries in the ancestry of a name, from most specific to root.
function findRegistries(
    bytes calldata name
) external view returns (IRegistry[] memory)
name
bytes
DNS-encoded name to traverse
registries
IRegistry[]
Array of registries in label-order (most specific to root)

Example: Walking the Registry Tree

// Get all registries for "sub.alice.eth"
bytes memory name = hex"037375620561616c696365036574680000";

IRegistry[] memory registries = universalResolver.findRegistries(name);

// Result array structure:
// [0] = registry for "sub.alice.eth" (or null if doesn't exist)
// [1] = registry for "alice.eth" (or null if doesn't exist)
// [2] = registry for "eth"
// [3] = root registry
This is useful for understanding the full resolution path and checking permissions at each level.

Traversal Examples

Here are the results for different name queries:
findRegistries("") = [<root>]
findRegistries("eth") = [<eth>, <root>]
findRegistries("alice.eth") = [<alice>, <eth>, <root>]
// If sub.alice.eth doesn't exist but alice.eth does
findRegistries("sub.alice.eth") = [null, <alice>, <eth>, <root>]

Resolution Flow

The resolution process follows this hierarchy:

Integration Examples

Basic Name Resolution

import {UniversalResolverV2} from "./UniversalResolverV2.sol";
import {IAddrResolver} from "@ens/contracts/resolvers/profiles/IAddrResolver.sol";

contract MyContract {
    UniversalResolverV2 public resolver;
    
    function resolveAddress(bytes memory name) external view returns (address) {
        // Find the resolver for this name
        (address resolverAddr, bytes32 node,) = resolver.findResolver(name);
        
        if (resolverAddr == address(0)) {
            return address(0);
        }
        
        // Query the resolver for the address
        return IAddrResolver(resolverAddr).addr(node);
    }
}

Multi-Chain Resolution

import {IAddressResolver} from "@ens/contracts/resolvers/profiles/IAddressResolver.sol";

function resolveMultiChainAddress(
    bytes memory name,
    uint256 coinType
) external view returns (bytes memory) {
    (address resolverAddr, bytes32 node,) = universalResolver.findResolver(name);
    
    if (resolverAddr == address(0)) {
        return "";
    }
    
    return IAddressResolver(resolverAddr).addr(node, coinType);
}

Registry Verification

function verifyCanonicalOwnership(
    bytes memory name,
    address expectedRegistry
) external view returns (bool) {
    IRegistry actualRegistry = universalResolver.findCanonicalRegistry(name);
    return address(actualRegistry) == expectedRegistry;
}

Gas Optimization

The UniversalResolverV2 uses several optimization techniques:
1

Immutable Root Registry

The root registry reference is immutable, saving gas on every call
2

Library-Based Logic

Core resolution logic in LibRegistry is compiled inline
3

Early Exit

Resolution stops as soon as a resolver is found
4

Memory Efficiency

Uses bytes memory for name passing to minimize copies

Security Considerations

Registry Trust: The UniversalResolverV2 trusts the root registry and all registries in the hierarchy. Ensure the root registry is properly secured and only creates trusted subregistries.
View Functions: All resolution functions are view, meaning they cannot modify state and are safe to call from other contracts or off-chain.

IRegistry

Registry interface

PermissionedResolver

ENS v2 resolver implementation

Build docs developers (and LLMs) love