IDotnsRegistry
Title: IDotnsRegistry
Author: Parity
Minimal on-chain registry for hierarchical name ownership and resolution.
Tokenised second-level nodes are owned through the registrar (ERC-721); subnodes are
owned directly by the address stored in Record.owner.
Note: security-contact: admin@parity.io
Functions
setSubnodeOwner
Creates or reassigns a subnode and assigns its owner.
Callable only by the current owner of record.parentNode, otherwise
Notes:
-
reverts: NotAuthorised. The new owner address must be non-zero, otherwise
-
reverts: NotAllowed.
record.subLabelmust be a single canonical DNS label (otherwise @custom:reverts InvalidLabel) andrecord.parentLabelmust be a name path of at mostStringUtils.MAX_NAME_PATH_OCTETSoctets whose namehash matchesrecord.parentNode(otherwise -
reverts: ParentLabelMismatch). Subnodes are parent-sovereign: the current
record.parentNodeowner may reassign or rotate a subnode's resolver at any time without the prior subnode owner's consent. On reassignment the resolver pointer is reset to the protocol-registered default reverse resolver so a prior subnode owner's resolver cannot be inherited by the next holder (records on other resolver contracts are keyed by node and are not cleared by this function; downstream consumers should gate resolver reads on current ownership). Indexes the subnode under the new owner'sLabelStorekeyed by the namehashedsubnodeso off-chain consumers can enumerate names per address. Indexing into the owner'sLabelStoreis governed byrecord.persist(see @custom:struct SubnodeRecord); the ownership and resolver record is written either way. Arecord.persistof false defers the store write and is restricted to registered controllers (@custom:function IDotnsRegistrar.controllers), otherwise @custom:reverts NotAuthorised. Creating a subname, as opposed to reassigning one that resolves, requires it to sit at mostMAX_SUBNAME_DEPTHlevels below its second-level name, otherwise @custom:reverts SubnameTooDeep. A subname still on record but cut off by a clear above it is created again from nothing: its versions are bumped, emitting -
emits: RecordsCleared and @custom:emits SubnamesCleared, so none of its old records or subnames come back. Emits @custom:emits NewOwner on each successful assignment.
function setSubnodeOwner(SubnodeRecord calldata record) external returns (bytes32 subnode);
setSubnodeResolver
Sets the resolver for an existing subnode.
Callable only by the current owner of record.parentNode, otherwise
Notes:
-
reverts: NotAuthorised. The subnode owner can still update the resolver directly via
setResolver. Both entry points emit the sameNewResolver(subnode, ...)event, so the parent can silently override a subnode owner's chosen resolver: this is the parent-sovereign hierarchy applied to resolution. Off-chain consumers that surface trust signals to subnode owners should treat any resolver rotation as a re-attestation prompt.record.subLabelmust be a single canonical DNS label (otherwise @custom:reverts InvalidLabel) andrecord.parentLabelmust be a name path of at mostStringUtils.MAX_NAME_PATH_OCTETSoctets whose namehash matchesrecord.parentNode(otherwise -
reverts: ParentLabelMismatch). The resulting subnode must exist and resolve, otherwise @custom:reverts NotAuthorised. Emits @custom:emits NewResolver on success.
function setSubnodeResolver(SubnodeResolverRecord calldata record) external;
setOwner
Creates or resets a node record for a tokenised second-level registration.
Restricted to the registrar's controllers, otherwise @custom:reverts NotAuthorised.
newOwner must be non-zero (otherwise @custom:reverts NotAllowed) and must match
the ERC-721 owner reported by the registrar (otherwise
Notes:
-
reverts: NotAuthorised). The function is callable both on a fresh registration and on every reclaim from escrow: each call rewrites
records[node].resolverto the protocol-registered default reverse resolver so a prior owner's resolver pointer (and the records keyed under it) cannot be inherited by the next holder across that recycle. A secondary-market ERC-721transferFromdoes not call the registry, so a name sold directly keeps the seller's resolver pointer until the buyer overwrites it. That covers the node itself and not the nodes beneath it: a subname stores its own owner, and @custom:function isAuthorised returns on that owner before consulting the registrar, so after the sale the seller keeps write authority over every subname whose stored owner is still the seller, until the buyer deletes each one through @custom:function deleteSubname or clears them all at once through @custom:function clearSubnames. Reassigning one through -
function: setSubnodeOwner keeps its records and the subnames beneath it. A subname created for someone else stays with that owner and leaves the seller no write path. The set is derivable from
-
emits: NewOwner, which indexes the parent node. Stores
owner = address(0)as a sentinel so reads delegate toIDotnsRegistrar.ownerOfand ERC-721 transfers remain authoritative. Emits -
emits: NodeTransferred on success.
function setOwner(bytes32 node, address newOwner) external;
setResolver
Sets or clears the resolver for a node.
Callable only by the current node owner, otherwise @custom:reverts NotAuthorised.
For tokenised nodes, authorisation falls back to ERC-721 owner / approved /
operator-for-all via the registrar. The registry does not validate
resolverAddr against any interface or code presence; off-chain consumers must
verify resolver shape before trusting reads. Emits @custom:emits NewResolver on
success.
function setResolver(bytes32 node, address resolverAddr) external;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | |
resolverAddr | address | Resolver contract address (zero clears). |
parentOf
Returns the recorded parent of a subname.
Zero for a tokenised name, for a node that was never created or was deleted, and for a subname whose parent was never recorded and that has not been linked since (@custom:function linkSubname). A backfill reads it to find the subnames still to link.
function parentOf(bytes32 node) external view returns (bytes32 parent);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
Returns
| Name | Type | Description |
|---|---|---|
parent | bytes32 | Recorded parent node, or zero. |
owner
Returns the owner of a node.
For tokenised nodes the stored owner is the zero sentinel; the implementation falls
back to IDotnsRegistrar.ownerOf(uint256(node)). A subname that no longer resolves,
because it or a name above it was cleared or deleted, has no owner and returns zero, as
do @custom:function resolver, @custom:function recordExists and
Note: function: isAuthorised for it.
function owner(bytes32 node) external view returns (address);
resolver
Returns the resolver of a node, or zero when the node does not resolve.
function resolver(bytes32 node) external view returns (address);
recordExists
Returns whether a node exists and resolves.
function recordExists(bytes32 node) external view returns (bool);
isAuthorised
Returns whether account is authorised to manage node.
For subnodes, authority is the explicit stored owner. For tokenised nodes it is the ERC-721 owner, an address approved for the token, or an operator approved for all of the owner's tokens via the registrar. This is the canonical authorisation check the registry enforces on owner-gated entry points; sibling contracts may consult it so a single registrar-level approval delegates management across the protocol. Returns false for a node that does not exist.
function isAuthorised(bytes32 node, address account) external view returns (bool authorisedFlag);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
account | address | Address whose authority is being checked. |
Returns
| Name | Type | Description |
|---|---|---|
authorisedFlag | bool | True when account may manage node. |
clearRecords
Clears the records of node on the resolvers that check recordVersion
(DotnsResolver and DotnsContentResolver), and points it back at the default
resolver.
Callable by anyone @custom:function isAuthorised admits for node, otherwise
Notes:
-
reverts: NotAuthorised. Constant cost whatever the number of records: the node's record version is bumped rather than any record being visited. Subnames are untouched; see @custom:function clearSubnames. Emits @custom:emits RecordsCleared and
-
emits: NewResolver.
function clearRecords(bytes32 node) external;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node to clear. |
clearSubnames
Clears every subname beneath node, at any depth, together with their records.
Callable by anyone @custom:function isAuthorised admits for node, otherwise
Note: reverts: NotAuthorised. Constant cost whatever the size or depth of the subtree: the node's subname version is bumped, and a subname resolves only while the version it was created under is still current at every level above it. A cleared subname can be created again through @custom:function setSubnodeOwner, and starts with no records and no subnames. The node's own records are untouched; see @custom:function clearRecords. Emits @custom:emits SubnamesCleared.
function clearSubnames(bytes32 node) external;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node whose subnames are cleared. |
clear
Clears every record and every subname of node.
Equivalent to @custom:function clearRecords followed by @custom:function clearSubnames in one call, with the same authority. The escrow calls it on release, once it holds the token, which is what makes a released name start clean for whoever gets it next.
function clear(bytes32 node) external;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node to clear. |
deleteSubname
Deletes one subname of parentNode, together with its records and every subname
beneath it.
Callable by anyone @custom:function isAuthorised admits for parentNode, otherwise
Notes:
-
reverts: NotAuthorised: subnames are parent-sovereign, so the parent decides, not the subname's holder. The subname's record and parent link are zeroed, releasing their storage deposit to the signer of the transaction; one version slot stays so the subname, created again, starts empty. The subname need not resolve: one a clear has already cut off can still be deleted by whoever controls
parentNodenow, after a transfer included. Subnames beneath it are cleared but keep their storage, and once their own parent stops resolving nobody can delete them, so freeing every deposit means deleting from the deepest subname up, each by whoever holds that subname's parent, before clearing anything above them. A subname must exist there, otherwise -
reverts: SubnameNotFound. Emits @custom:emits NewOwner with a zero owner, the deletion convention indexers already follow, and @custom:emits RecordsCleared and
-
emits: SubnamesCleared for the subname.
function deleteSubname(bytes32 parentNode, bytes32 labelhash) external;
Parameters
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | Parent of the subname. |
labelhash | bytes32 | Labelhash of the subname under parentNode. |
linkSubname
Records the parent of a subname whose parent was never recorded, so that clearing an ancestor reaches it.
Permissionless: the parent is proven by the namehash, since subnode is
keccak256(parentNode, labelhash), so a caller cannot link a subname anywhere else.
The subname is linked as created under its parent's first subname version, so a subname
whose ancestor was cleared before it was linked reads as cleared once it and every
level between it and that ancestor are linked; an unlinked level still ends the walk
as resolving. Every successful write path that keeps the subname and names its parent
(@custom:function setSubnodeOwner, @custom:function setSubnodeResolver) links it as a
side effect; this entry point covers the rest. @custom:reverts SubnameNotFound when
no subname exists there. A subname already linked is left as it is. Emits
Note: emits: SubnameLinked when it links.
function linkSubname(bytes32 parentNode, bytes32 labelhash) external;
Parameters
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | Parent of the subname. |
labelhash | bytes32 | Labelhash of the subname under parentNode. |
revokeName
Clears a tokenised name that is being revoked and deletes its record.
Callable only by the registrar, otherwise @custom:reverts NotRegistrar. The registrar calls it from @custom:function IDotnsRegistrar.revoke and
Notes:
-
function: IDotnsRegistrar.revokeIssued before burning the token. Bumps both versions of
node, so its records read as unset and every linked subname beneath it stops resolving, including after the name is registered again. A subname that was never linked keeps resolving until @custom:function linkSubname links it. Deletes the record, so the node reads as unregistered (@custom:function owner returns zero) rather than deferring to the burned token. Emits @custom:emits RecordsCleared, -
emits: SubnamesCleared, @custom:emits NewResolver with a zero resolver and
-
emits: NodeTransferred with a zero owner.
function revokeName(bytes32 node) external;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node of the name being revoked. |
revokeSubname
Deletes one subname of parentNode, together with its records and every subname
beneath it, on governance's authority rather than the parent's.
Callable only under a Root origin, otherwise @custom:reverts NotRoot. Does what
Notes:
-
function: deleteSubname does, with the same events and the same
-
reverts: SubnameNotFound, whoever holds the parent. A device name, a subname of a container the PoP controller holds, reverts @custom:reverts IssuedByGateway. A second-level name has no stored owner in this contract, so it reverts
-
reverts: SubnameNotFound here and is revoked through
-
function: IDotnsRegistrar.revoke.
function revokeSubname(bytes32 parentNode, bytes32 labelhash) external;
Parameters
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | Parent of the subname. |
labelhash | bytes32 | Labelhash of the subname under parentNode. |
recordVersion
Returns whether node resolves and the record version its records are written
under.
Resolvers stamp each write with recordsVersion and treat a record whose stamp
differs, or any record of a node that does not resolve, as unset. A tokenised name
resolves while its record exists; a subname resolves while no ancestor has cleared or
deleted it.
Version 0 is the version every node starts at, and a record stamped 0 matches it, so a
node that was never cleared reads every record stamped 0.
Checking a subname walks its ancestors, so the cost grows with its depth.
function recordVersion(bytes32 node) external view returns (bool live, uint64 recordsVersion);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
Returns
| Name | Type | Description |
|---|---|---|
live | bool | True when node currently resolves. |
recordsVersion | uint64 | Record version of node. |
Events
NewOwner
Emitted when a new subnode owner is set.
event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Parent node. |
label | bytes32 | Labelhash of the created subnode. |
owner | address |
NodeTransferred
Emitted when ownership of a node is transferred.
event NodeTransferred(bytes32 indexed node, address owner);
NewResolver
Emitted when a resolver is set or updated.
event NewResolver(bytes32 indexed node, address resolver);
RecordsCleared
Emitted when every record of node stops resolving.
Records live on resolver contracts and cannot be listed, so nothing is erased: the
node's record version moves to version and every resolver that stamps its writes with
Note:
function: recordVersion reads anything written under an earlier version as
unset. Consumers that cache records must drop every cached record of node on this
event.
event RecordsCleared(bytes32 indexed node, uint64 version);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node whose records were cleared. |
version | uint64 | Record version the node's records are now written under. |
SubnamesCleared
Emitted when every subname beneath node, at any depth, stops resolving.
One event covers the whole subtree, because the subtree cannot be listed. Descendants
emit nothing of their own: consumers that index subnames must treat this event as the
removal of every node below node, together with their records.
event SubnamesCleared(bytes32 indexed node, uint64 version);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node whose subnames were cleared. |
version | uint64 | Subname version a new child of node is created under. |
SubnameLinked
Emitted when a subname created without a recorded parent is linked to it.
event SubnameLinked(bytes32 indexed node, bytes32 indexed parent);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | The subname. |
parent | bytes32 | Its parent node. |
Errors
NotAllowed
Thrown when an invalid (zero) address is provided.
error NotAllowed();
NotAuthorised
Thrown when the caller is not authorised.
error NotAuthorised();
NotRegistryController
Thrown when the caller is not the registry controller.
error NotRegistryController();
NodeAlreadyOwned
Thrown when attempting to create a node that already exists.
error NodeAlreadyOwned(bytes32 node);
SubnameNotFound
Thrown when a subname that should exist under a parent does not, or no longer resolves.
error SubnameNotFound(bytes32 node);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | The subname. |
SubnameTooDeep
Thrown when a new subname would sit deeper below its second-level name than
MAX_SUBNAME_DEPTH allows.
error SubnameTooDeep(uint256 depth, uint256 maxDepth);
Parameters
| Name | Type | Description |
|---|---|---|
depth | uint256 | Depth the subname would have. |
maxDepth | uint256 | Deepest allowed. |
InvalidLabel
Thrown when a sublabel is not a canonical lowercase ASCII DNS label.
error InvalidLabel();
ParentLabelMismatch
Thrown when the supplied parent label does not match the parent node, or is not a valid name path.
The two cases share one error because both mean the caller's parentLabel cannot be
trusted to name parentNode: a mismatched namehash, a malformed segment, or a path
over StringUtils.MAX_NAME_PATH_OCTETS. That last bound is on the path in octets, so
it limits nesting only through how much of the budget each level spends. It exists
because the composed full name is stored in a LabelStore row that has no delete
path.
error ParentLabelMismatch();
NotRoot
Thrown when @custom:function revokeSubname is called outside a Root origin.
error NotRoot();
NotRegistrar
Thrown when @custom:function revokeName is called by anything other than the registrar.
error NotRegistrar(address caller);
IssuedByGateway
Thrown when @custom:function revokeSubname targets a device name.
Device names are subnames of containers the PoP controller holds, and the gateway keeps state about them that only @custom:function IDotnsPopGovernance.revokeName clears.
error IssuedByGateway(bytes32 node);
Structs
SubnodeRecord
Record describing a subnode creation request.
struct SubnodeRecord {
bytes32 parentNode;
string subLabel;
string parentLabel;
address owner;
bool persist;
}
Properties
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | |
subLabel | string | Human readable subnode label e.g "alice". |
parentLabel | string | Canonical parent name without the TLD suffix e.g. bob or child.bob. At most StringUtils.MAX_NAME_PATH_OCTETS octets. The bound is on the path in octets, so it limits nesting only through how much of that budget each level spends. |
owner | address | Address to assign as owner of the created subnode. |
persist | bool | Whether to index the subnode into the owner's LabelStore, deploying it on demand. When false the ownership and resolver record is still written but the store is left untouched, and an authorised store writer backfills the label later by deploying the owner's store and writing to it. Deferring the store write this way is restricted to registered controllers (@custom:function IDotnsRegistrar.controllers): any other caller must pass true, otherwise @custom:reverts NotAuthorised, because it could never backfill a deferred label. The registry writes the store only on creation or on a reassignment to a new owner, so a later same-owner re-call with true does not backfill it; the authorised writer backfills it directly. |
Record
Record describing the state of a node.
struct Record {
address owner;
address resolver;
bool exists;
}
Properties
| Name | Type | Description |
|---|---|---|
owner | address | Address that owns the node, or address(0) sentinel for tokenised nodes. |
resolver | address | Address of the resolver associated with the node. |
exists | bool | Whether the node has been explicitly created. |
SubnodeResolverRecord
Record describing a subnode resolver update request.
struct SubnodeResolverRecord {
bytes32 parentNode;
string subLabel;
string parentLabel;
address resolver;
}
Properties
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | |
subLabel | string | Human-readable subnode label e.g "alice". |
parentLabel | string | Canonical parent name without the TLD suffix e.g bob or child.bob. At most StringUtils.MAX_NAME_PATH_OCTETS octets. The bound is on the path in octets, so it limits nesting only through how much of that budget each level spends. |
resolver | address | Resolver contract address (zero clears). |