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

Permissioned Resolver

In ENSv1, most names shared a single Public Resolver contract. In ENSv2, each account gets its own resolver instance, deployed as a UUPS-upgradeable proxy. All names owned by the same account share one resolver. This gives name owners fine-grained control over who can set which records, and enables new features like record aliasing.

What Changed from ENSv1

FeatureENSv1 Public ResolverENSv2 Permissioned Resolver
DeploymentSingle shared contractPer-account proxy instances
PermissionsOwner controls all recordsPer-record-type roles via EAC
AliasingNot supportedBuilt-in name aliasing with cycle protection
Record clearingclearRecords() by ownerRecord versioning
UpgradeabilityNot upgradeableUUPS proxy pattern

Supported Record Types

The Permissioned Resolver supports all standard ENS record types:

RecordGetterSetterStandard
Address (ETH)addr(bytes32 node)setAddr(bytes32 node, address)ENSIP-1
Address (multichain)addr(bytes32 node, uint256 coinType)setAddr(bytes32 node, uint256, bytes)ENSIP-9
Texttext(bytes32 node, string key)setText(bytes32 node, string, string)ENSIP-5
Content hashcontenthash(bytes32 node)setContenthash(bytes32 node, bytes)ENSIP-7
Name (reverse)name(bytes32 node)setName(bytes32 node, string)EIP-181
Public keypubkey(bytes32 node)setPubkey(bytes32 node, bytes32, bytes32)EIP-619
ABIABI(bytes32 node, uint256)setABI(bytes32 node, uint256, bytes)EIP-205
InterfaceinterfaceImplementer(bytes32, bytes4)setInterface(bytes32, bytes4, address)ENSIP-8
Datadata(bytes32 node, string key)setData(bytes32 node, string, bytes)ENSIP-24

EAC Integration

All permissions are managed through Enhanced Access Control.

Roles

Each record type has its own role, allowing you to delegate specific record-setting permissions to different accounts:

RoleValueScopePurpose
ROLE_SET_ADDR1 << 0root, name, or recordSet address records
ROLE_SET_TEXT1 << 4root, name, or recordSet text records
ROLE_SET_CONTENTHASH1 << 8root or nameSet content hash
ROLE_SET_PUBKEY1 << 12root or nameSet public key
ROLE_SET_ABI1 << 16root or nameSet ABI data
ROLE_SET_INTERFACE1 << 20root or nameSet interface records
ROLE_SET_NAME1 << 24root or nameSet reverse name
ROLE_SET_ALIAS1 << 28rootSet name aliases
ROLE_CLEAR1 << 32root or nameClear all records
ROLE_SET_DATA1 << 36root, name, or recordSet data records
ROLE_UPGRADE1 << 124rootAuthorize proxy upgrades

Each role has a corresponding admin role at role << 128 (e.g., ROLE_SET_TEXT_ADMIN = (1 << 4) << 128). In TypeScript, use 1n << 4n for the bigint equivalent.

Role Bitmap Composer

Granting and Revoking Roles

Use the authorize* functions below to manage permissions (pass true to grant, false to revoke).

FunctionScopeGrants/revokes
authorizeNameRoles(toName, roleBitmap, account, grant)Name-levelAny role(s) on a specific name
authorizeTextRoles(toName, key, account, grant)Record-levelROLE_SET_TEXT for a specific text key
authorizeDataRoles(toName, key, account, grant)Record-levelROLE_SET_DATA for a specific data key
authorizeAddrRoles(toName, coinType, account, grant)Record-levelROLE_SET_ADDR for a specific coin type

All four functions take a DNS-encoded name (bytes) as the first parameter (toName). For example, authorizeNameRoles(toName, ROLE_SET_TEXT, account, true) grants the account permission to set any text key on that name, while authorizeTextRoles(toName, "avatar", account, true) restricts it to only the avatar key. To revoke either, make the same call with false.

ROLE_SET_ADDR, ROLE_SET_TEXT, and ROLE_SET_DATA support both levels of scoping. A name-level grant (via authorizeNameRoles) covers all keys or coin types on that name. A record-level grant (via authorizeTextRoles, authorizeDataRoles, or authorizeAddrRoles) covers only the specific key or coin type. When checking permissions, the resolver allows the action if the account has the role at either scope, so a name-level grant is a superset of any record-level grant.

Resource Scheme

Unlike the registry's labelhash-based resources, resolver resources are opaque hashes with no version structure, and the anyId polymorphism used in the registry does not apply here. The resolver computes each EAC resource as a hash of the name's namehash and a record-type identifier:

resource = keccak256(node, part)

Where node is the namehash of the full name (e.g., namehash("alice.eth")) and part is computed via partHash:

Record typePart computation
Name-level (any record)bytes32(0)
Text keypartHash(key) = keccak256(bytes(key))
Data keypartHash(key) = keccak256(bytes(key))
Coin typepartHash(coinType) = keccak256(abi.encode(coinType))

When checking permissions, the resolver looks across four combinations of node and part and allows the action if any grant the required role. Using the shorthand resource(node, part) for keccak256(node, part):

Any recordSpecific record
Any nameROOT_RESOURCE (when both node and part are zero)resource(0, part)
Specific nameresource(namehash, 0)resource(namehash, part)

authorizeNameRoles sets the role on resource(namehash, 0) (second row, first column), while authorizeTextRoles with a specific key sets it on resource(namehash, partHash(key)) (second row, second column). A role granted at ROOT_RESOURCE covers all four cells.

Resource Calculator

Aliasing

ENSv2 introduces record aliasing. Within a single resolver instance, you can make one name's records point to another name's records, so they always resolve identically without duplicating data.

Setting an Alias

Both setAlias parameters are DNS-encoded names. The alias and target must use the same resolver instance:

Viem
import { createWalletClient, http, toHex } from 'viem'
import { packetToBytes } from 'viem/ens'
import { mainnet } from 'viem/chains'
 
const wallet = createWalletClient({ chain: mainnet, transport: http() })
 
// Make wallet.eth resolve to the same records as alice.eth
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'setAlias',
  args: [
    toHex(packetToBytes('wallet.eth')),  // from: the alias name
    toHex(packetToBytes('alice.eth')),   // to: the canonical name
  ],
})
 
// To remove an alias, set the target to empty
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'setAlias',
  args: [toHex(packetToBytes('wallet.eth')), '0x00'],
})

setAlias requires ROLE_SET_ALIAS, which is a root-only role. This ensures only the resolver's admin can set up aliases. To alias names that were registered with separate resolvers, first point them to the same resolver via setResolver on the registry.

Cycle Protection

The resolver detects self-referential aliases (A aliased to itself) and applies them only once. However, longer alias cycles (e.g., A -> B -> A or A -> B -> C -> A) are not detected and will cause an out-of-gas revert during resolution. Avoid creating circular alias chains.

Use Cases

  • Multiple domains, same records: point wallet.eth, brand.eth, and company.eth to the same resolver, then alias them so they share one set of records
  • Name migration: alias an old name to a new one so existing references continue to work
  • Subname delegation: let subnames of a parent (e.g., pay.alice.eth, nft.alice.eth) resolve to the parent's records without duplicating them

Note that aliasing at the resolver level is different from namespace aliasing at the registry level. Resolver aliasing shares records; registry aliasing shares entire namespaces.

Record Versioning

The resolver supports record versioning. The clearRecords(node) function increments the version, effectively clearing all records at once without individual delete calls. This is useful when you want a clean slate, for example when transferring a name to a new owner.

UUPS Upgradeability

Each resolver instance is a UUPS proxy pointing to a shared implementation contract. The ROLE_UPGRADE (root-only) controls who can upgrade the implementation. This means:

  • All resolver instances share the same logic, keeping deployment costs low
  • The implementation can be upgraded to support new record types in the future
  • Individual name owners can upgrade their resolver if they hold the upgrade role

See Verifiable Factory for how resolver proxies are deployed.

Code Examples

Each account deploys its own resolver instance. See Deploying a Resolver Proxy for a code example showing how to deploy a Permissioned Resolver through the Verifiable Factory. The examples below assume you already have a resolverAddress.

Setting and Reading Records

The Permissioned Resolver implements the same record interfaces as ENSv1 resolvers (addr, text, contenthash, etc.), so existing viem patterns work unchanged. The only difference is that each account deploys its own resolver instance rather than sharing one:

Viem
import { createPublicClient, createWalletClient, http, namehash } from 'viem'
import { normalize } from 'viem/ens'
import { mainnet } from 'viem/chains'
 
const client = createPublicClient({ chain: mainnet, transport: http() })
const wallet = createWalletClient({ chain: mainnet, transport: http() })
 
const name = normalize('alice.eth')
const node = namehash(name)
 
// Set an ETH address
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'setAddr',
  args: [node, '0x1234...'],
})
 
// Set a text record
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'setText',
  args: [node, 'avatar', 'https://example.com/avatar.png'],
})
 
// Read them back using viem's built-in ENS functions
const ethAddr = await client.getEnsAddress({ name })
const avatar = await client.getEnsText({ name, key: 'avatar' })

Delegating a Single Text Key

A name owner can grant a dApp permission to set only a specific text record, for example allowing it to update the avatar key without giving access to any other records:

Viem
import { createWalletClient, http, namehash, toHex } from 'viem'
import { packetToBytes } from 'viem/ens'
import { mainnet } from 'viem/chains'
 
const wallet = createWalletClient({ chain: mainnet, transport: http() })
 
// authorize* functions take DNS-encoded names; record setters take namehashes
const dnsName = toHex(packetToBytes('alice.eth'))
const node = namehash('alice.eth')
 
// Grant a dApp permission to set ONLY the "avatar" text key on alice.eth
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'authorizeTextRoles',
  args: [dnsName, 'avatar', dappAddress, true],
})
 
// The dApp can now set the avatar record...
await dappWallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'setText',
  args: [node, 'avatar', 'https://example.com/avatar.png'],
})
 
// ...but attempting to set any other key will revert
// setText(node, 'description', '...') → reverts with EACUnauthorizedAccountRoles

To grant access to all text keys on a name (not just one), use authorizeNameRoles with ROLE_SET_TEXT:

Viem
const ROLE_SET_TEXT = 1n << 4n
 
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'authorizeNameRoles',
  args: [dnsName, ROLE_SET_TEXT, dappAddress, true],
})

Revoking Permissions

To revoke a permission, call the same authorize* function with false as the last argument:

Viem
// Revoke the dApp's permission to set the "avatar" text key
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'authorizeTextRoles',
  args: [dnsName, 'avatar', dappAddress, false],
})
 
// Revoke name-level ROLE_SET_TEXT (all text keys)
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'authorizeNameRoles',
  args: [dnsName, ROLE_SET_TEXT, dappAddress, false],
})

Locking a Record Permanently

Because each role has a corresponding admin role, an owner can make a record type permanently immutable by revoking both the role and its admin from themselves. Without the admin role, nobody can grant the role back.

Viem
import { createWalletClient, http, namehash } from 'viem'
import { mainnet } from 'viem/chains'
 
const wallet = createWalletClient({ chain: mainnet, transport: http() })
const node = namehash('alice.eth')
 
const ROLE_SET_CONTENTHASH = 1n << 8n
const ROLE_SET_CONTENTHASH_ADMIN = ROLE_SET_CONTENTHASH << 128n
 
// Set the content hash one final time
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'setContenthash',
  args: [node, contenthashBytes], // your encoded content hash
})
 
// Permanently lock: revoke both the role and its admin in one call
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'revokeRootRoles',
  args: [ROLE_SET_CONTENTHASH | ROLE_SET_CONTENTHASH_ADMIN, ownerAddress], // your address
})
// The content hash can never be changed again, even by the owner

Clearing All Records

The clearRecords function increments the record version for a name, effectively clearing all records at once without individual delete calls. This is useful when transferring a name to a new owner or starting fresh:

Viem
const node = namehash('alice.eth')
 
// Check the current version
const versionBefore = await client.readContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'recordVersions',
  args: [node],
})
 
// Clear all records in one call
await wallet.writeContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'clearRecords',
  args: [node],
})
 
// The version is incremented; all previous records now return empty
const versionAfter = await client.readContract({
  address: resolverAddress,
  abi: permissionedResolverAbi,
  functionName: 'recordVersions',
  args: [node],
})
// versionAfter === versionBefore + 1n

Reference

Write Functions

setText(node, key, value)Set a text record. Requires ROLE_SET_TEXT.
setAddr(node, coinType, addressBytes)Set a multichain address record. Requires ROLE_SET_ADDR.
setAddr(node, addr_)Set the ETH address record (convenience overload for coin type 60).
setContenthash(node, hash)Set the content hash. Requires ROLE_SET_CONTENTHASH.
setName(node, name)Set the reverse name. Requires ROLE_SET_NAME.
setPubkey(node, x, y)Set the SECP256k1 public key. Requires ROLE_SET_PUBKEY.
setABI(node, contentType, value)Set ABI data. Requires ROLE_SET_ABI.
setInterface(node, interfaceId, implementer)Set an interface implementer. Requires ROLE_SET_INTERFACE.
setData(node, key, value)Set an arbitrary data record. Requires ROLE_SET_DATA.
setAlias(fromName, toName)Set a name alias (DNS-encoded). Requires ROLE_SET_ALIAS on ROOT_RESOURCE.
clearRecords(node)Clear all records for a node by bumping the version. Requires ROLE_CLEAR.
authorizeNameRoles(toName, roleBitmap, account, grant)Grant or revoke roles on a name.
authorizeTextRoles(toName, key, account, grant)Grant or revoke ROLE_SET_TEXT for a specific text key.
authorizeDataRoles(toName, key, account, grant)Grant or revoke ROLE_SET_DATA for a specific data key.
authorizeAddrRoles(toName, coinType, account, grant)Grant or revoke ROLE_SET_ADDR for a specific coin type.
initialize(admin, roleBitmap, setters)Initialize the proxy instance. Grants the given roles to the admin on ROOT_RESOURCE, then executes the setter calls via multicall with permission checks bypassed.
multicall(calls)Execute multiple write operations in a single transaction.

View Functions

addr(node)Get the ETH address for a node.
addr(node, coinType)Get the multichain address for a node.
text(node, key)Get a text record value.
contenthash(node)Get the content hash.
name(node)Get the reverse name.
pubkey(node)Get the SECP256k1 public key.
ABI(node, contentTypes)Get ABI data.
data(node, key)Get an arbitrary data record.
interfaceImplementer(node, interfaceId)Get the interface implementer address.
hasAddr(node, coinType)Check whether an address record exists.
getAlias(name)Get the alias target for a name.
recordVersions(node)Get the current record version number.
resolve(fromName, fromData)Resolve a name by applying alias rewriting (if set) and dispatching to the appropriate resolver profile. Implements IExtendedResolver.

Events

AddrChanged(node, a)ETH address changed.
AddressChanged(node, coinType, newAddress)Multichain address changed.
TextChanged(node, indexedKey, key, value)Text record changed.
DataChanged(node, indexedKey, key, indexedData)Data record changed.
ContenthashChanged(node, hash)Content hash changed.
NameChanged(node, name)Reverse name changed.
PubkeyChanged(node, x, y)Public key changed.
ABIChanged(node, contentType)ABI data changed.
InterfaceChanged(node, interfaceId, implementer)Interface implementer changed.
VersionChanged(node, newVersion)Records cleared (version bumped).
AliasChanged(indexedFromName, indexedToName, fromName, toName)Alias set or removed.
NamedResource(resource, name)EAC resource linked to a name.
NamedTextResource(resource, name, keyHash, key)EAC resource linked to a text key.
NamedDataResource(resource, name, keyHash, key)EAC resource linked to a data key.
NamedAddrResource(resource, name, coinType)EAC resource linked to a coin type.