ERC-4834 Hierarchical domains
Final · Names & metadata · created 2022-02-22
A minimal name resolution standard that treats a contract itself as a domain and converts human-readable names into addresses. Each level can attach the access control it wants.
Why it is needed
Ethereum addresses are long hex strings, which are hard for people to read or remember. Schemes such as ENS (ERC-137), which attach names to addresses, are used for this, but the source points to two problems: ENS has a relatively complex structure and no standardized access control. If the way of deciding who can create, change, and delete each name is not fixed by a standard, then even when different levels want to use different management approaches, there is no common interface, so apps and wallets find it hard to handle them consistently. This standard defines a minimal interface of just two functions needed for name resolution and has every domain, from the root and top-level domains (TLDs) to subdomains, implemented by the same contract, so that names can be resolved with a simple iterative algorithm like DNS.
What to implement
function hasDomain(string[] memory name) external view returns (bool);: Returns true or false for whether the given name (in right-to-left order) has a subdomain.function getDomain(string[] memory name) external view returns (address);: Returns the address of the subdomain for the given name. It must revert if hasDomain is false for the name.event SubdomainCreate(address indexed sender, string name, address subdomain);: An event that must be emitted when a new subdomain is created.event SubdomainUpdate(address indexed sender, string name, address subdomain, address oldSubdomain);: An event that must be emitted when the address a domain points to changes, and it also records the previous address.event SubdomainDelete(address indexed sender, string name, address subdomain);: An event that must be emitted when a domain link is removed.function createDomain(string memory name, address subdomain) external payable;: Creates a subdomain with the given name. It must revert if the caller lacks permission or the subdomain already exists.function setDomain(string memory name, address subdomain) external;: Changes the address that an existing subdomain points to. It must revert if the caller lacks permission or the domain does not exist.function deleteDomain(string memory name) external;: Deletes a subdomain. It must revert if the domain does not exist or the caller lacks permission.function canCreateDomain(address updater, string memory name, address subdomain) external view returns (bool);: Tells whether an account can create a subdomain of the given name. It must return false if the name already exists.function canSetDomain(address updater, string memory name, address subdomain) external view returns (bool);: Tells whether an account can change a subdomain. If the target subdomain implements the access control extension, the canMoveSubdomain decision on that side must also be reflected.function canDeleteDomain(address updater, string memory name) external view returns (bool);: Tells whether an account can delete a subdomain. If the target implements the access control extension, the canDeleteSubdomain decision must also be reflected.function subdomainByIndex(uint256 index) external view returns (string memory);: Looks up a subdomain name by index. It must revert if the count is unknown or infinite.function totalSubdomains() external view returns (uint256);: Returns the total number of subdomains. It must revert if the count is unknown or infinite.function canMoveSubdomain(address updater, string memory name, IDomain parent, address newSubdomain) external view returns (bool);: The subdomain itself decides whether an account can move this subdomain from its current parent to somewhere else. The parent's canSetDomain can call it.function canDeleteSubdomain(address updater, string memory name, IDomain parent) external view returns (bool);: The subdomain itself decides whether an account can remove the link of a subdomain of this domain. The parent's canDeleteDomain can call it.
In depth
Every level is the same domain contract
In this standard, a domain is a contract that implements the IDomain interface. Subdomains, TLDs, and even the root are all implemented as domains in the same way. This makes name resolution a simple iterative algorithm similar to DNS. The required interface is only two functions, hasDomain and getDomain, and both take the name as a string array (string[]) in right-to-left order. getDomain must revert when hasDomain(name) is false.
Name resolution algorithm
Split a name such as "a.b.c" by the delimiter to make ["a", "b", "c"], and initialize domain to the root and path to an empty list. Take the last element of the array, add it to path, and then call domain.hasDomain(path). If it returns false, resolution fails, and if true, domain is replaced with domain.getDomain(path). Repeat until all the split pieces are gone. Because there is no limit on nesting depth, the source gives an example where the root holds z and z holds y, and so on, so that even a 36-level name is valid. Because the contracts along the resolution path fully control resolution, flexible implementations are possible, such as a domain that returns a different address depending on the time of day.
Optional extension Registerable: registration and changes
It defines creation (createDomain), update (setDomain) and deletion (deleteDomain) of subdomains, along with three events (SubdomainCreate, SubdomainUpdate, SubdomainDelete). Each write function must revert if its corresponding query function (canCreateDomain, canSetDomain, canDeleteDomain) returns false or if the existence condition is not met. canCreateDomain must return false when hasDomain(name) is true, and canSetDomain and canDeleteDomain must return false when hasDomain(name) is false. createDomain is declared payable, so an implementation may charge a registration fee.
Optional extension Enumerable: listing subdomains
subdomainByIndex(uint256 index) returns the subdomain name at the given index, and totalSubdomains() returns the total count. Both functions must revert when the number of subdomains is unknown or infinite. With this extension, external callers can iterate over the subdomains a domain holds.
Optional extension Access Control: permissions on the subdomain side
This extension lets a subdomain decide permissions about itself. canMoveSubdomain returns whether an account can detach this subdomain from its current parent and replace it with another address, and canDeleteSubdomain returns whether an account can remove the link. The parent domain's canSetDomain and canDeleteDomain are designed to call these functions, so a subdomain is not bound only by the parent's access control. The Rationale in the original text also states that these functions are advisory in nature. For example, a domain that has an owner can implement an owner override so that the domain can be recovered if a key is lost.
Why a simple ownable interface was not used
To keep the design as general as possible, the standard does not adopt a simple owner (ownable) interface. If an owner model is needed, it can be implemented that way. This structure allows different governance at each level. The original text gives an example combination: the root domain is governed by a DAO that distributes non-transferable tokens equally, a TLD by a stake-token DAO, domains below it by a single owner, and subdomains by an owner tied to an NFT.
Relationship with ENS
This standard has a similar purpose to ENS (ERC-137) but takes a different approach. Unlike ENS's resolution algorithm, name resolution in this standard is fully controlled by the contracts along the resolution path. The original text explains that this behavior is more intuitive for users. On compatibility, it states that this standard is general enough to support ENS, but ENS is not general enough to support this standard. This standard has reached Final status.
Things to watch out for
- Malicious canMoveSubdomain (black hole): Moving a subdomain with setDomain can be risky. If a malicious subdomain unexpectedly returns false from canMoveSubdomain, ownership of the domain can effectively be locked. Conversely, if it is a backdoor that unexpectedly returns true, the owner of that contract can hijack the domain. Clients should warn if canMoveSubdomain or canDeleteSubdomain of a new subdomain changes to false, but since these are functions, their values can change before and after linking. Therefore, it is recommended to always audit the source code of the new subdomain before calling setDomain.
- Parent domain's control over resolution: A parent domain fully controls name resolution of its subdomains. Even if a domain is linked at a.b.c, b.c can replace a.b.c with any domain depending on its code, and c can replace b.c itself with any domain. Before acquiring a pre-linked domain, it is recommended to audit that contract together with every parent up to the root.