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
| Feature | ENSv1 Public Resolver | ENSv2 Permissioned Resolver |
|---|---|---|
| Deployment | Single shared contract | Per-account proxy instances |
| Permissions | Owner controls all records | Per-record-type roles via EAC |
| Aliasing | Not supported | Built-in name aliasing with cycle protection |
| Record clearing | clearRecords() by owner | Record versioning |
| Upgradeability | Not upgradeable | UUPS proxy pattern |
Supported Record Types
The Permissioned Resolver supports all standard ENS record types:
| Record | Getter | Setter | Standard |
|---|---|---|---|
| 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 |
| Text | text(bytes32 node, string key) | setText(bytes32 node, string, string) | ENSIP-5 |
| Content hash | contenthash(bytes32 node) | setContenthash(bytes32 node, bytes) | ENSIP-7 |
| Name (reverse) | name(bytes32 node) | setName(bytes32 node, string) | EIP-181 |
| Public key | pubkey(bytes32 node) | setPubkey(bytes32 node, bytes32, bytes32) | EIP-619 |
| ABI | ABI(bytes32 node, uint256) | setABI(bytes32 node, uint256, bytes) | EIP-205 |
| Interface | interfaceImplementer(bytes32, bytes4) | setInterface(bytes32, bytes4, address) | ENSIP-8 |
| Data | data(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:
| Role | Value | Scope | Purpose |
|---|---|---|---|
ROLE_SET_ADDR | 1 << 0 | root, name, or record | Set address records |
ROLE_SET_TEXT | 1 << 4 | root, name, or record | Set text records |
ROLE_SET_CONTENTHASH | 1 << 8 | root or name | Set content hash |
ROLE_SET_PUBKEY | 1 << 12 | root or name | Set public key |
ROLE_SET_ABI | 1 << 16 | root or name | Set ABI data |
ROLE_SET_INTERFACE | 1 << 20 | root or name | Set interface records |
ROLE_SET_NAME | 1 << 24 | root or name | Set reverse name |
ROLE_SET_ALIAS | 1 << 28 | root | Set name aliases |
ROLE_CLEAR | 1 << 32 | root or name | Clear all records |
ROLE_SET_DATA | 1 << 36 | root, name, or record | Set data records |
ROLE_UPGRADE | 1 << 124 | root | Authorize 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).
| Function | Scope | Grants/revokes |
|---|---|---|
authorizeNameRoles(toName, roleBitmap, account, grant) | Name-level | Any role(s) on a specific name |
authorizeTextRoles(toName, key, account, grant) | Record-level | ROLE_SET_TEXT for a specific text key |
authorizeDataRoles(toName, key, account, grant) | Record-level | ROLE_SET_DATA for a specific data key |
authorizeAddrRoles(toName, coinType, account, grant) | Record-level | ROLE_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 type | Part computation |
|---|---|
| Name-level (any record) | bytes32(0) |
| Text key | partHash(key) = keccak256(bytes(key)) |
| Data key | partHash(key) = keccak256(bytes(key)) |
| Coin type | partHash(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 record | Specific record | |
|---|---|---|
| Any name | ROOT_RESOURCE (when both node and part are zero) | resource(0, part) |
| Specific name | resource(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:
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, andcompany.ethto 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:
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:
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 EACUnauthorizedAccountRolesTo grant access to all text keys on a name (not just one), use authorizeNameRoles with ROLE_SET_TEXT:
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:
// 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.
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 ownerClearing 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:
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 + 1nReference
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. |