Skip to content

Universal Resolver

A swiss army knife for resolution.

Overview

The Universal Resolver is a smart contract that simplifies the process of resolving ENS names. It's the recommended way to implement ENS resolution in modern libraries.

Adopting the universal resolver will make the transition to ENSv2 seamless.

Implementation Guide

The universal resolver is the only ENS smart contract that should to be hardcoded into an application for resolution. You can find the latest deployment addresses here.

This guide assumes that your application already supports CCIP Read.

Forward Resolution

To resolve one or more records for a name, use the resolve(bytes name, bytes data) method which returns (bytes data, address resolver).

The name argument is the DNS-encoded version of the name. Make sure to normalize the name first, as well! For example, given the name My.Name.eth:

  1. Normalize:
    • My.Name.eth -> my.name.eth
  2. DNS Encode:
    • my.name.eth -> 0x026d79046e616d650365746800

The data argument is a single ABI-encoded call to the resolver for that name, or a multicall encoded via the following interface:

interface IMulticallable {
  function multicall(bytes[] calldata data) external view returns (bytes[] memory results);
}

For example, if you want to resolve the ETH address and description text record for nick.eth, your logic to generate the data parameter would look something like this:

import { namehash, normalize } from 'viem/ens'
import { encodeFunctionData, parseAbi } from 'viem/utils'
 
const simpleResolverAbi = parseAbi([
  'function addr(bytes32 node) view returns (address)',
  'function text(bytes32 node, string key) view returns (string)',
])
 
const multicallAbi = parseAbi([
  'function multicall(bytes[] data) returns (bytes[] results)',
])
 
const name = normalize('nick.eth')
const node = namehash(name)
 
const resolverCalls = [
  {
    abi: simpleResolverAbi,
    functionName: 'addr',
    args: [node],
  },
  {
    abi: simpleResolverAbi,
    functionName: 'text',
    args: [node, 'description'],
  },
] as const
 
const data = encodeFunctionData({
  abi: multicallAbi,
  functionName: 'multicall',
  args: [resolverCalls.map((call) => encodeFunctionData(call))],
})

Learn about standard resolver methods for ABI-encoding that should be added to simpleResolverAbi in production.

The response you'll get back from resolve will have to be decoded against the multicall ABI, then further decoded against the base resolver ABI. This results in the ETH address and value of the description text record. The rest of the logic would look something like this:

import { createPublicClient, decodeFunctionResult, http, toHex } from 'viem'
import { mainnet } from 'viem/chains'
import { packetToBytes } from 'viem/ens'
 
// ...adding from the above example
 
const dnsEncodedName = toHex(packetToBytes(name))
 
const universalResolverAbi = parseAbi([
  'error ResolverNotFound(bytes name)',
  'error ResolverNotContract(bytes name, address resolver)',
  'error UnsupportedResolverProfile(bytes4 selector)',
  'error ResolverError(bytes errorData)',
  'error ReverseAddressMismatch(string primary, bytes primaryAddress)',
  'error HttpError(uint16 status, string message)',
  'function resolve(bytes name, bytes data) view returns (bytes result, address resolver)',
  'function reverse(bytes lookupAddress, uint256 coinType) view returns (string primary, address resolver, address reverseResolver)',
])
 
const client = createPublicClient({
  chain: mainnet,
  transport: http(),
})
 
const resolveRes = await client.readContract({
  abi: universalResolverAbi,
  address: '0xaBd80E8a13596fEeA40Fd26fD6a24c3fe76F05fB',
  functionName: 'resolve',
  args: [dnsEncodedName, data],
})
 
const decodedMulticall = decodeFunctionResult({
  abi: multicallAbi,
  functionName: 'multicall',
  data: resolveRes[0],
})
 
const decodedRes = decodedMulticall.map((res, i) =>
  decodeFunctionResult({
    abi: simpleResolverAbi,
    functionName: resolverCalls[i].functionName,
    data: res,
  })
)
 
// decodedRes[0] = "0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5"
// decodedRes[1] = "Lead developer of ENS & Ethereum Foundation alum. Certified rat tickler. he/him."

Reverse Resolution

To reverse-resolve an address to an ENS name (go from address to name), call the reverse (bytes lookupAddress, uint256 coinType) method which returns (string primary, address resolver, address reverseResolver).

The lookupAddress argument is the Ethereum address of the account you want to fetch the primary name for. The coinType argument defines the chain you'd like to fetch the reverse record from. See Multichain Addresses for more information about the coinType argument.

reverse internally checks that the name forward resolves to the address you're looking up, so your implementation doesn't need to do any additional checks and should be quite straightforward. It might look something like this:

import { createPublicClient, http, parseAbi } from 'viem'
import { mainnet } from 'viem/chains'
 
const address = '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5'
 
const universalResolverAbi = parseAbi([
  'error ResolverNotFound(bytes name)',
  'error ResolverNotContract(bytes name, address resolver)',
  'error UnsupportedResolverProfile(bytes4 selector)',
  'error ResolverError(bytes errorData)',
  'error ReverseAddressMismatch(string primary, bytes primaryAddress)',
  'error HttpError(uint16 status, string message)',
  'function resolve(bytes name, bytes data) view returns (bytes result, address resolver)',
  'function reverse(bytes lookupAddress, uint256 coinType) view returns (string primary, address resolver, address reverseResolver)',
])
 
const client = createPublicClient({
  chain: mainnet,
  transport: http(),
})
 
const [primaryName] = await client.readContract({
  abi: universalResolverAbi,
  address: '0xaBd80E8a13596fEeA40Fd26fD6a24c3fe76F05fB',
  functionName: 'reverse',
  args: [address, 60n],
})
 
// primaryName = "nick.eth"

Batch Gateways

The Universal Resolver utilizes a batch gateway to perform parallel EIP-3668 (CCIP-Read) requests and mitigate offchain server issues. CCIPBatcher.sol is the recommended batch gateway client implementation. The protocol and server implementation is defined in ENSIP-21: Batch Gateway Offchain Lookup Protocol (BGOLP).

The latest batch gateways can be queried from the Universal Resolver via batchGateways() which returns (string[] gateways).

  1. External Batch Gateway https://ccip-v3.ens.xyz is a trustless service operated by ENS Labs which receives batch gateway requests, performs the supplied CCIP-Read requests in parallel, bundles up the responses or failures, and replies to the caller. This service acts like an open proxy. This mechanism functions in ALL clients that support CCIP-Read.

  2. Local Batch Gateway x-batch-gateway:true is a special-purpose URL which notifies ENSIP-21 aware clients that the OffchainLookup is a BGOLP request and can be handled locally without using the External Batch Gateway service. The client follows the same process but the requests originate locally without an additional network hop to an external service. For unaware clients, the URL is ignored by the Client Lookup Protocol in EIP-3668.