Subnames in ENSv2 can live purely as data in a resolver or become tokens with their own owners, expiries and permissions. If you control every subname beneath your ENS name, one resolver may be all you need. A registry becomes useful when ownership differs, when individual names should point at their own resolver or hand out subnames of their own, or when you want those names represented as tokens. The examples below cover the most common patterns, moving from simple records to tokenized names, and show how these choices affect control over a subname space.
Every subname setup is shaped by two contracts, even when only one of them ends up deployed.
- A resolver stores the records that names resolve to. These records are organized as record bundles, which names link to.
- A registry turns names into tokens with owners, expiries and roles. It also stores which resolver serves each name.
Both come as finished implementations and can be deployed cheaply, as often as needed, through the Verifiable Factory. When choosing between configurations, the guiding principle is that the registry graph is there to express ownership differences. Wherever all descendants of a name share one owner, subnames can simply remain data.
Records for your subnames can always live on your resolver, whether you use the names yourself or point them at other people. A registry under your name enters the picture once those subnames should have owners of their own. In both contracts, what a configuration ultimately spells out is who is permitted to do what.
Concretely, the permissions in question are: who may register a subname, who may edit which record and who may hand these abilities to someone else. In ENSv2, registries and resolvers manage all of this through the same system, called Enhanced Access Control (EAC). Since EAC is implemented as a self-contained module, it can also be adopted and extended by your own contracts. For the purposes of this article, however, it is enough to understand the following four ideas.
Roles. A role is the permission to perform one specific action and nothing more. For example, an account holding ROLE_SET_TEXT on a resolver can set text records, while an account holding ROLE_REGISTRAR on a registry can register subnames. "Account" here means a plain Ethereum address, that is, an EOA or a contract: roles are always granted to addresses.
Admin roles. Every role has an admin counterpart. Holding ROLE_SET_TEXT_ADMIN does not allow its holder to set anything by itself. Instead, it allows them to grant or revoke ROLE_SET_TEXT to any account, including themselves. This separation of doing and delegating is intentional: it makes it possible to give a collaborator the ability to work without also giving them the ability to pass that work on.
Resources. A role is always granted on a so-called resource. A resource is nothing more than a number identifying the thing a permission is scoped to, and each contract is free to choose what its resources stand for. A registry derives one resource per name it manages: holding ROLE_SET_RESOLVER on the resource of alice.wallet.eth in wallet.eth's registry means you can change the resolver of exactly this name and no other. A resolver, on the other hand, derives one resource per record argument: holding ROLE_SET_TEXT on the resource of the avatar key (which is literally the hash keccak("avatar")) means you can edit avatar records and nothing else. The permission system is the same in both contracts, but the scope differs.
The root resource. One resource is reserved: the ROOT_RESOURCE, which stands for the contract as a whole. Note that every EAC contract has its own root resource, i.e. the ROOT_RESOURCE of a registry and the ROOT_RESOURCE of a resolver are unrelated to each other. A role granted to an account on the ROOT_RESOURCE counts as held by that account on every resource at once, including resources that do not even exist yet. Accordingly, whenever a contract checks a permission for a given account, it looks in two places: the account's roles on the specific resource and its roles on the ROOT_RESOURCE. If the role is present in either place, the check passes.
// pseudo-code, simplified from the EAC implementation
// a roleBitmap encodes a set of roles as a bitmask
// hasRoles answers: does account hold every role in roleBitmap on resource?
hasRoles(resource, roleBitmap, account):
effective = roles[ROOT_RESOURCE][account] | roles[resource][account]
return effective & roleBitmap == roleBitmap
On a freshly deployed contract, the ROOT_RESOURCE is also the only resource with any role assignments at all: on every other resource, no account holds anything until a role is explicitly granted there. Root roles can therefore be thought of as the master keys of a contract. Typically, the creator starts out holding all of them, together with their admin counterparts. Giving them up, or deliberately keeping them, is what ultimately shapes who controls a subname space. In fact, most of the configuration decisions in the examples below come down to exactly this question: which roles stay on the ROOT_RESOURCE, and in whose hands?
This diagram shows the resource allocation for a resolver contract deployed by Alice. Alice's account holds ROLE_SET_TEXT together with its admin counterpart on the ROOT_RESOURCE, which means the role counts on every text record resource and she can grant or revoke it for other accounts. Concretely, Alice can edit any text record of any name on her resolver. The address associated with the bot only holds ROLE_SET_TEXT on the "avatar" resource. The bot can therefore edit the avatar text record of any name on this resolver, but no other key, and since it holds no admin role, it cannot pass this ability on to anyone else.
In ENSv1, if you wanted to give pay.wallet.eth a record through the standard resolver, you first had to create a registry entry for it, even though the name belonged to you all along. Subnames without registry entries were possible as well, but only by deploying a custom wildcard resolver or by running an offchain gateway. In ENSv2, this is simply how the standard resolver works: its setter functions take full names of arbitrary depth, which means that names like pay.wallet.eth or l2.pay.wallet.eth exist the moment you write a record for them. All you need is one token in your wallet and one resolver, without any registry graph to maintain.
This works because resolution walks the registry graph from the ENS root registry and stops at the deepest entry it can find. For l2.pay.wallet.eth, that entry is wallet.eth itself, so the query is handed to wallet.eth's resolver together with the full name. One resolver therefore covers every name below wallet.eth, at any depth.
One token in the wallet. Subnames of any depth live as link-table entries and records inside the resolver, with no extra contracts.
In this setup the subnames are entirely yours: Alice can be the beneficiary of alice.wallet.eth, but every record on it is written and controlled by you.
The following configuration is a refinement of the pure-data setup: names that have no record of their own fall back to the so-called default record. Suppose a community or a product wants every subname to serve the same website, avatar and status. In this setup, these values only need to be written once, and every subname, present or future, resolves to them without any per-name setup.
Same setup as Example 1 plus one element: names without a record of their own fall back to the default record.
The default record is written with the ordinary setters: they accept the empty name, encoded as a single zero byte 0x00, and the record created for it is the one every name without a record of its own falls back to.
The fallback applies per name, not per field: as soon as a name gets any record of its own, it stops falling back entirely. Unlinking the name from its record puts it back on the default.
The moment subname owners start to differ, ownership needs to be expressed onchain, and this is exactly what a UserRegistry is for. With a UserRegistry in place, subnames become transferable tokens with their own expiries and roles, and each owner can, if allowed, point their name at a resolver of their own choosing.
A UserRegistry turns subnames into owned tokens. Each entry carries its own resolver pointer, here both point at one shared resolver. Entries are shown with full names for readability. Onchain, the registry stores only the label, alice or bob, and the rest of the name follows from where the registry is attached.
It's worth highlighting that you don't have to make this decision up front: a pure-data setup can be tokenized after the fact by deploying a registry and attaching it under your name, without breaking any of the existing records.
Between no entry and owned token there is a third state: a name can be reserved. A reserved entry is a registration without an owner. No token exists, but the entry still carries an expiry and can point at a resolver or a subregistry. This is useful in two ways. As a routing node, a reserved name lets you delegate a subtree or assign a resolver without minting anything. As a hold, it takes a label out of circulation: reserved names cannot be registered through the normal path, and converting one into an owned token requires a role of its own. You can therefore maintain a list of protected or premium names that lapses automatically unless renewed.
Tokenizing subnames introduces a new dimension of configurability. The registration of a subname takes a role bitmap, i.e. the set of roles the new owner receives on their name, and this bitmap decides what kind of product a subname is. The registry-side roles correspond exactly to the questions an operator has to answer:
ROLE_SET_RESOLVER: Should the owner be able to point the name at their own resolver, thereby opting out of whatever shared record setup exists?ROLE_SET_SUBREGISTRY: Should the owner be able to attach their own registry and hand out subnames of their subname?ROLE_CAN_TRANSFER_ADMIN: Should it be possible to transfer the name at all, i.e. to sell it or to move it to another wallet? Without this role, the token is bound to its owner.- The admin counterparts of each role: Should the owner be able to delegate these abilities onward?
A single UserRegistry covers the whole spectrum. If you withhold everything, you have minted a non-transferable membership badge on rails the operator controls. If you grant everything, the owner can manage their name like a first-class citizen.
The other half of this dimension is determined by what the operator keeps on the ROOT_RESOURCE of the UserRegistry itself. Recall that roles held there apply to every subname in this registry. This means that an operator holding ROLE_SET_RESOLVER on it can override any owner's resolver choice, which can be useful for a managed service, but is disqualifying for a trustless one. Handing out strong per-name roles while keeping the matching root roles is not a contradiction, but it does constitute a trust model, and it should be a deliberate one.
At this point, one more possibility opens up, and it hinges on a detail from the EAC section: roles are granted to addresses, and an address can also be a contract. If you grant ROLE_REGISTRAR (and ROLE_RENEW) on the UserRegistry's ROOT_RESOURCE to a registrar contract, your registration policy becomes code. Who may register, under what conditions, for how long and at what price is then whatever the contract enforces. In this way, registration turns into a self-service flow that runs without you: users call the registrar, the registrar validates the request and collects payment, and the registry mints the subname. ROLE_REGISTRAR alone covers all of this. The bitmap granted at registration needs no admin roles and may contain any roles, admin counterparts included, because the grant only applies to the registered name's own resource. This is not an exotic extension. In fact, it is exactly how .eth itself works, with the ETH Registrar being a contract holding these two roles on the ROOT_RESOURCE of the .eth registry.
The tokenized setup from Example 3 with one addition: a registrar contract holding ROLE_REGISTRAR and ROLE_RENEW on the registry makes registrations and renewals self-service, under whatever rules the contract enforces.
Independently of tokenization, resolver roles can be scoped to a single setter argument, for example to one text key or to one coin type of the address record. This allows an operator to hand an automated writer exactly one field. For instance, a payment processing agent can hold ROLE_SET_ADDRESS for the BTC coin type only: it can then rotate the BTC receive address after every incoming payment, but it cannot touch the ETH address, avatars or any other record.
The grant itself takes just one call:
// grant the agent the setAddress role for the BTC coin type only
resolver.grantSetterRoles(
abi.encodeCall(resolver.setAddress, ("", COIN_TYPE_BTC, "")),
paymentProcessingAgentAddress
);
The first argument of grantSetterRoles is an encoded call to the setter that is being delegated. The resolver uses the function selector (setAddress) and coin type (0 for BTC) to scope the permission. The name and address bytes are ignored, so the example uses empty placeholders. The resolver then grants the agent ROLE_SET_ADDRESS on the resource of coin type 0, i.e. on address records for BTC.
However, it's important to pay close attention to the scope of the grant here: it is per argument, not per name. On a shared resolver, the agent's role covers the BTC address of alice.wallet.eth, of bob.wallet.eth, and of every name added to the resolver later. Scoping a writer to specific names is a deployment decision rather than a role decision, and this is what the next example is about.
grantSetterRoles scopes a role to one argument: the agent rotates the BTC address on every record in this resolver and nothing else.
While roles cannot confine a resolver writer to specific names, deployments can. There is one prerequisite, however: pointing a name at a resolver of its own is a registry-level concept, so this configuration requires a registry under wallet.eth, i.e. the setup from Example 3. Note that the entries do not even need owners for this: a reserved, ownerless entry can carry a resolver pointer as well, so the operator can partition names without handing out any tokens.
With this in place, the recipe is simple. Because every permission lives inside one resolver instance, we point the names the agent should manage at one instance, everything else at a second one and grant the agent its role on the first instance only.
Let's make this concrete by applying the recipe to the setup from Example 4: alice.wallet.eth stays on resolver A, where the payment processing agent holds ROLE_SET_ADDRESS for BTC. bob.wallet.eth, on the other hand, points at resolver B, where the agent holds nothing. Within resolver A, the agent's reach is unchanged: it still covers the BTC address of every name served there. Resolver B, however, and every name on it, is simply outside of its world. Notably, the role system never had to know about names at all: which names a writer can touch is decided by which resolver each name points to.
In other words, the resolver instance is the trust boundary. Group names by who is allowed to write to them, give each group its own resolver, and grant roles per instance.
alice.wallet.eth and bob.wallet.eth are entries in wallet.eth's registry, each pointing at its own resolver. The agent holds its role on resolver A only.
These patterns are neither exhaustive nor mutually exclusive, and most real setups combine them. The following questions can help you choose a starting point.
Do all subnames belong to you? In this case, stay with pure data (Examples 1 and 2). You get one token, one resolver and records at any depth. A useful self-check is the following: if you find yourself registering tokens to your own address with identical permissions on every name, then you did not need a registry in the first place. The ownership structure you are expressing is "everything is mine", and plain records already express exactly that. This check measures necessity, nothing more. If you want your subnames to be tokens, because tokens show up in wallets and can be traded, that is a reason of its own, and Example 3 covers you just the same.
Do owners differ, or should names be sellable? In this case, tokenize (Example 3). Deploy a UserRegistry and treat the role bitmap at registration as your product design: withhold the transfer role for membership badges, grant the resolver, subregistry and transfer roles for full ownership, and decide deliberately which override roles you keep on the registry's ROOT_RESOURCE. For the cases in between, ownerless reserved entries are available as well, providing registry routing without handing out tokens.
Do machines write records? In this case, scope them to the one field they need (Example 4). This works in any of the setups above. If the per-field scope is still too broad, partition the names across resolver instances and grant the writer its role on only one of them (Example 5). Remember that the resolver instance is the trust boundary.
Not sure yet? In this case, start small. A pure-data setup can be upgraded to tokens after the fact without breaking any existing records, so you do not owe the future a registry today. The one decision that is hard to walk back is giving up your root roles. Therefore, keep them until you know what your subname space is supposed to be, and give them up once you do.
The ENSv2 documentation covers every building block from this article in more depth, and each of the following pages comes with code examples for the concepts discussed here:
- Permissioned Registry: registration, roles and the lifecycle of tokenized subnames
- Permissioned Resolver: records, linking and the delegation of single record fields
- For Contract Developers: builds a minimal registrar contract, like the one described in this article, end to end