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 name gets its own resolver instance, deployed as a UUPS-upgradeable proxy. 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-name proxy instances
PermissionsOwner controls all recordsPer-record-type roles via EAC
AliasingNot supportedBuilt-in name aliasing with cycle protection
Record clearingclearRecords() by ownerProfile 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)EIP-2544

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

Both grantRoles() and revokeRoles() are disabled on the Permissioned Resolver. Instead, all role management goes through authorize* functions that take a bool grant parameter: pass true to grant, false to revoke.

FunctionScopeGrants/revokes
authorizeNameRoles(name, roleBitmap, account, grant)Name-levelAny role(s) on a specific name
authorizeTextRoles(name, key, account, grant)Record-levelROLE_SET_TEXT for a specific text key
authorizeDataRoles(name, key, account, grant)Record-levelROLE_SET_DATA for a specific data key
authorizeAddrRoles(name, 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. For example, authorizeNameRoles(name, ROLE_SET_TEXT, account, true) grants the account permission to set any text key on that name, while authorizeTextRoles(name, "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

One of the most powerful new features in ENSv2 is record aliasing. You can make one name's records point to another name's records, so they always resolve identically without duplicating data.

Setting an Alias

// Make wallet.eth resolve to the same records as alice.eth
resolver.setAlias(
    dnsEncode("wallet.eth"),  // from: the alias name
    dnsEncode("alice.eth")    // to: the canonical name
);

The setAlias function requires ROLE_SET_ALIAS, which is a root-only role. This ensures only the resolver's admin can set up aliases.

Cycle Protection

The resolver includes built-in cycle detection. If you try to create a chain of aliases that loops back on itself (e.g., A -> B -> C -> A), the resolution will detect the cycle and stop. This prevents infinite loops during resolution.

Use Cases

  • Multiple domains, same records: point wallet.eth, brand.eth, and company.eth all to the same set of records
  • Name migration: alias an old name to a new one so existing references continue to work
  • Shared infrastructure: multiple names can share resolver records managed by a single entity

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

Multi-Profile Support

The resolver supports profile 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.

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(name, roleBitmap, account, grant)Grant or revoke roles on a name.
authorizeTextRoles(name, key, account, grant)Grant or revoke ROLE_SET_TEXT for a specific text key.
authorizeDataRoles(name, key, account, grant)Grant or revoke ROLE_SET_DATA for a specific data key.
authorizeAddrRoles(name, coinType, account, grant)Grant or revoke ROLE_SET_ADDR for a specific coin type.

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.

Events

AddrChanged(node, addr)ETH address changed.
AddressChanged(node, coinType, addressBytes)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, version)Records cleared (version bumped).
AliasChanged(fromName, toName, indexedFromName, indexedToName)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.

Code Examples

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],
})

Resolving Through an Alias

When a name is aliased, records are resolved through the Universal Resolver V2. The alias rewrite happens during the resolve() call, not when reading records directly from the resolver:

Viem
import {
  createPublicClient,
  decodeFunctionResult,
  encodeFunctionData,
  http,
  namehash,
  parseAbi,
  toHex,
} from 'viem'
import { packetToBytes } from 'viem/ens'
import { mainnet } from 'viem/chains'
 
const client = createPublicClient({ chain: mainnet, transport: http() })
 
const resolverAbi = parseAbi([
  'function addr(bytes32 node) view returns (address)',
  'function text(bytes32 node, string key) view returns (string)',
])
 
const universalResolverAbi = parseAbi([
  'function resolve(bytes name, bytes data) view returns (bytes, address)',
])
 
// Suppose wallet.eth is aliased to alice.eth.
// Resolving wallet.eth via the Universal Resolver returns alice.eth's records:
const calldata = encodeFunctionData({
  abi: resolverAbi,
  functionName: 'addr',
  args: [namehash('wallet.eth')],
})
 
const [resultData] = await client.readContract({
  address: universalResolverAddress,
  abi: universalResolverAbi,
  functionName: 'resolve',
  args: [toHex(packetToBytes('wallet.eth')), calldata],
})
 
const address = decodeFunctionResult({
  abi: resolverAbi,
  functionName: 'addr',
  data: resultData,
})
// address === alice.eth's address
 
// Subname aliasing works too: sub.wallet.eth resolves to sub.alice.eth's records
// without any additional configuration.