Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Preparing for ENSv2

Everything you need to know to prepare your application for ENSv2.

ENSv2 introduces a redesigned architecture and improved multi-chain interoperability. To ensure your application works seamlessly with ENSv2, you'll need to make a few key updates.

The good news? For most applications, preparing for ENSv2 is as simple as updating to the latest version of a supported library. At the time of writing, not all libraries have added ENSv2 support yet. Here's the current status:

Universal Resolver

Even though ENSv2 is designed for multi-chain, all resolution still starts on Ethereum Mainnet. There is a new Universal Resolver that acts as the canonical entry point. This is an upgradable proxy contract, owned be the ENS DAO, so its address won't change in the future if its implementation is changed.

Your application needs to use this new Universal Resolver in order to be ready for ENSv2. As mentioned above, updating to the latest version of your supported web3 library handles this automatically.

Learn more about the Universal Resolver here and about the resolution process in general here.

Testing Universal Resolver Support

To test if your integration uses the Universal Resolver, try resolving the address for ur.integration-tests.eth. It should return 0x2222222222222222222222222222222222222222. If it instead returns 0x1111111111111111111111111111111111111111, you likely need to update your web3 library.

Offchain and L2 Resolution with CCIP Read

ENSv1 already supports delegating resolution from Ethereum Mainnet to an L2 or completely offchain using CCIP Read (ERC-3668). All the libraries mentioned above implement CCIP Read. However, not all integrations handle it properly.

In a nutshell, CCIP Read defers resolution to a gateway. Think of a gateway as an HTTP API. The response of the gateway can be verified with a read-call to the ENS contracts on Ethereum Mainnet (or Sepolia for testing). This means that your application needs to be able to send HTTP requests as part of the ENS resolution process. As mentioned above, this is already handled by the web3 libraries in the background.

Learn more about CCIP-Read, Offchain and L2 resolvers here.

Testing CCIP Read Support

To test if your integration properly implements CCIP Read, try resolving test.offchaindemo.eth. It should return the address 0x779981590E7Ccc0CFAe8040Ce7151324747cDb97.

DNS Names and Name Detection

ENS supports importing DNS names into ENS, allowing legacy domain names to work alongside .eth names. It's important that your application correctly also detects DNS names.

Common Mistake: Only Matching .eth

Many integrations check if the input ends with .eth in order to detect an ENS name:

if (input.endsWith('.eth') {
  // ...
}

This is incorrect because it excludes DNS names imported into ENS (like ensfairy.xyz).

Correct Pattern: Match All Valid Domains

Instead, your integration should treat any dot-separated string as a potential ENS name. For example, a.co should be treated as a potential ENS name.

if (input.includes('.') && input.length > 2) {
  // ...
}

This pattern correctly matches:

  • .eth names like vitalik.eth
  • DNS names like ensfairy.xyz
  • Subdomains like ses.fkey.id
  • Emoji domains like 🦇️🔊️🦇️🔊️🦇️🔊️.eth

Learn more about DNS integration here. The full specification of name normalization is defined in ENSIP-15.

Multichain Considerations

Even if your application only operates on an L2 like Base, ENS resolution always starts on Ethereum Mainnet. This means you need to configure a L1 client alongside your L2 chain.

Configuring Both L2 and Mainnet

Here's how to set up your application to use Base (or another L2) while ensuring ENS resolution works correctly by including Mainnet:

Viem
import { createPublicClient, http, toCoinType } from 'viem'
import { base, mainnet } from 'viem/chains'
 
// Client for Base transactions
const baseClient = createPublicClient({
  chain: base,
  transport: http(),
})
 
// Client for ENS resolution on Mainnet
const mainnetClient = createPublicClient({
  chain: mainnet,
  transport: http(),
})
 
// Get the Base address for this ENS name
const baseAddress = await mainnetClient.getEnsAddress({
  name: 'test.ses.eth',
  coinType: toCoinType(base.id),
})

Chain-Specific Addresses

It is possible to configure a different address per chain for the same name:

  • test.ses.eth resolves to 0x2B0F09F23193de2Fb66258a10886B9f06903276c for Ethereum Mainnet, but
  • test.ses.eth resolves to 0x7d3a48269416507E6d207a9449E7800971823Ffa for Base.

From an application point of view it is important to be aware and always request the address for the correct chain, even on Ethereum Mainnet. All examples above explicitly set the coinType to Base, since they request the Base address for a given name.

If Your Application Writes to ENS

Reading ENS data is abstracted away by the libraries above. Writing is not: registering a name or setting a record is a direct contract interaction, and the write-side contracts change with ENSv2. At the time of writing, ENSv2 write support in libraries is limited to preview releases (ENSjs v5), so applications that write to ENS need to update these code paths themselves.

During the migration from ENSv1 to ENSv2, new registrations happen exclusively in ENSv2, while names that have not migrated yet keep their ENSv1 write paths, renewals included. Applications should support ENSv2, which every new and migrated name uses. Additionally keeping the ENSv1 write paths for unmigrated names is optional. Everything below can be tested against the ENSv2 deployment on Sepolia today.

OperationENSv1ENSv2
Register or renew a .eth nameETHRegistrarController, paid in ETHNew ETH Registrar: commit-reveal stays, but fees are paid in stablecoins and the grace period is 28 days
Set address, text or contenthashSetters on the name's configured resolverUnchanged, but the configured resolver is now typically a per-account Permissioned Resolver: see below
Change a name's resolversetResolver(node, ...) on the ENS registrysetResolver(tokenId, ...) on the registry that holds the name
Create subnamessetSubnodeRecord on the registry or Name Wrapperregister() on the parent name's subregistry
Transfer a nameERC-721 (unwrapped) or ERC-1155 (wrapped)ERC1155Singleton transfer with mutable token IDs
Set a primary nameReverse Registrar setNameSee Reverse Resolution

Some changes deserve special attention:

Registration Fees Are Paid in Stablecoins

The new ETH Registrar keeps the commit-reveal flow, but the fee is no longer sent as ETH along with the transaction: register and renew are paid in an approved stablecoin of the caller's choice. The Sepolia deployment currently accepts USDC (both Circle's Sepolia USDC and a freely mintable test USDC) and a test DAI. For a registration UI this means a token approval step before registering, and balance checks against the stablecoin instead of ETH.

Token IDs Are Mutable

If your application caches token IDs, this is a breaking change: in ENSv2 a name's token ID can change over its lifetime. Always resolve the name to its current token ID at transaction time instead of storing it, using findTokenId(label) on the registry that holds the name.

Never Hardcode a Resolver Address

When a name is resolved, only the resolver configured for that name in the registry is queried. Records written to any other resolver contract are never returned. Hardcoding a resolver address was therefore already an anti-pattern in ENSv1, but it usually worked anyway, because the configured resolver of most names is the shared Public Resolver. In ENSv2 resolvers are deployed per account, so a name's configured resolver is its owner's own deployment and a hardcoded shared address is guaranteed to be the wrong contract. Always look up the name's configured resolver at write time, and don't cache the result either: the resolver can be reconfigured at any point, for example when the name changes hands. Once found, the setter interface is the same. The same applies to registries: a name's subnames live in its individual subregistry, not in a single global registry contract, so obtain it fresh before writing.

Name Wrapper and Fuses

If your application manages names through the Name Wrapper (wrapping, unwrapping or burning fuses), read the Enhanced Access Control page: the wrapper's functionality is built into the core of ENSv2, and fuses are replaced by roles.