⚠️ This EIP is not recommended for general use or implementation as it is likely to change.

EIP-4834: Hierarchical Domain Standard Source

A standard for extremely generic name resolution.

AuthorPandapip1
Discussions-Tohttps://ethereum-magicians.org/t/erc-4834-hierarchical-domains-standard/8388
StatusDraft
TypeStandards Track
CategoryERC
Created2022-02-22
Requires 20, 137, 165, 173

Abstract

This is a standard for generic name resolution with access control. It permits a contract that implements EIP-4834 (referred to as a “domain” hereafter) to be addressable with a more human-friendly name, with a similar purpose to EIP-137 (referred to as “ENS” hereafter).

Unlike ENS, any program that resolves domains should treat domains as equivalent to their resolved addresses. In practice, this means users of DApps that implement EIP-4834 name resolution may specify an address that looks like dai.token instead of 0x6b175474e89094c44da98b954eedeac495271d0f. In this instance, dai.token and 0x6b175474e89094c44da98b954eedeac495271d0f are not different, unlike ENS, where names are simply keys to be hashed and inputted into a storage contract that then resolves the name to an address.

Another notable divergence from ENS is that access control can be arbitrarily complex. ENS domains have a defined owner that has full permission to create, update, and delete subdomains, as well as update the metadata of the domain in resolver contracts. While this can be made more strict by delegating control of the ENS domain to a smart contract, EIP-4834 takes a different approach, and permits any access control patterns to be implemented. This could allow for interesting use-cases, such as an EIP-20 token that could be “redeemed” for subdomains by linking permission to create subdomains to ownership of a specific amount of that token and having that much token burned when a domain is created.

Motivation

The advantage of this EIP over those existing standards is that it provides a minimal interface that supports name resolution, adds standardized access control, and has a simple architecture. ENS, although useful, has a comparatively complex architecture.

In addition, all domains (including subdomains, TLDs, and even the root itself) are actually implemented as domains, meaning that name resolution is a simple iterative algorithm.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Contract Interface

Solidity Interface with NatSpec & OpenZeppelin v4 Interfaces (also available at IDomain.sol):

interface IDomain is IERC165 {
    //// Events
    
    /// @notice     Must be emitted when a new subdomain is created (eg. through `createDomain`.)
    /// @param      sender msg.sender for createDomain
    /// @param      name name for createDomain
    /// @param      subdomain subdomain in createDomain
    event SubdomainCreate(address indexed sender, string name, address subdomain);

    /// @notice     Must be emitted when the resolved address for a domain is changed (eg. with `setDomain`)
    /// @param      sender msg.sender for setDomain
    /// @param      name name for setDomain
    /// @param      subdomain subdomain in setDomain
    /// @param      oldSubdomain the old subdomain
    event SubdomainUpdate(address indexed sender, string name, address subdomain, address oldSubdomain);

    /// @notice     Must be emitted when a domain is unmapped (eg. with `deleteDomain`)
    /// @param      sender msg.sender for deleteDomain
    /// @param      name name for deleteDomain
    /// @param      subdomain the old subdomain
    event SubdomainDelete(address indexed sender, string name, address subdomain);


    //// CRUD

    /// @notice     Query if a domain has a subdomain with a given name
    /// @param      name The subdomain to query
    /// @return     `true` if the domain has a subdomain with the given name, `false` otherwise
    function hasDomain(string memory name) external view returns (bool);

    /// @notice     Fetch the subdomain with a given name
    /// @dev        This should revert if `hasDomain(name)` is `false`
    /// @param      name The subdomain to fetch
    /// @return     The subdomain with the given name
    function getDomain(string memory name) external view returns (address);
    
    /// @notice     Create a subdomain with a given name
    /// @dev        This should revert if `canCreateDomain(msg.sender, name, pointer)` is `false` or if the domain exists
    /// @param      name The subdomain name to be created
    /// @param      subdomain The subdomain to create
    function createDomain(string memory name, address subdomain) external;

    /// @notice     Update a subdomain with a given name
    /// @dev        This should revert if `canSetDomain(msg.sender, name, pointer)` is `false` of if the domain doesn't exist
    /// @param      name The subdomain name to be updated
    /// @param      subdomain The subdomain to set
    function setDomain(string memory name, address subdomain) external;

    /// @notice     Delete the subdomain with a given name
    /// @dev        This should revert if the domain doesn't exist or if
    ///             `canDeleteDomain(msg.sender, name, this)` is `false`
    /// @param      name The subdomain to delete
    function deleteDomain(string memory name) external;


    //// Parent Domain Access Control

    /// @notice     Get if an account can create a subdomain with a given name
    /// @dev        This must return `false` if `hasDomain(name)` is `true`.
    /// @param      updater The account that may or may not be able to create/update a subdomain
    /// @param      name The subdomain name that would be created/updated
    /// @param      subdomain The subdomain that would be set
    /// @return     Whether an account can update or create the subdomain
    function canCreateDomain(address updater, string memory name, address subdomain) external view returns (bool);

    /// @notice     Get if an account can update or create a subdomain with a given name
    /// @dev        This must return `false` if `hasDomain(name)` is `false`.
    ///             If `getDomain(name)` is also a domain, this should return `false` if
    ///             `getDomain(name).canMoveSubdomain(msg.sender, this, subdomain)` is `false`.
    /// @param      updater The account that may or may not be able to create/update a subdomain
    /// @param      name The subdomain name that would be created/updated
    /// @param      subdomain The subdomain that would be set
    /// @return     Whether an account can update or create the subdomain
    function canSetDomain(address updater, string memory name, address subdomain) external view returns (bool);

    /// @notice     Get if an account can delete the subdomain with a given name
    /// @dev        This must return `false` if `hasDomain(name)` is `false`.
    ///             If `getDomain(name)` is a domain, this should return `false` if
    ///             `getDomain(name).canDeleteSubdomain(msg.sender, this, subdomain)` is `false`.
    /// @param      updater The account that may or may not be able to delete a subdomain
    /// @param      name The subdomain to delete
    /// @return     Whether an account can delete the subdomain
    function canDeleteDomain(address updater, string memory name) external view returns (bool);
}

Name Resolution

To resolve a name (like "a.b.c"), you first split it by the delimiter (resulting in something like ["a", "b", "c"]). Set domain initially to the root domain.

Pop off the last element of the array ("c"), then call domain.hasDomain(lastElement). If it’s false, then the domain resolution fails. Otherwise, set the domain to domain.getDomain(lastElement). Repeat until the list of split segments is empty.

There is no limit to the amount of nesting that is possible. For example, 0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z would be valid if the root contains z, and z contains y, and so on.

Here is a solidity function that resolves a name:

function resolve(string[] calldata splitName, IDomain root) public view returns (address) {
    IDomain current = root;
    for (uint i = splitName.length - 1; i >= 0; i--) {
        // Require that the current domain has a domain
        require(current.hasDomain(splitName[i]), "Name resolution failed: );
        // Resolve subdomain
        current = current.getDomain(splitName[i]);
    }
    return current;
}

Optional Extension: Enumerable

Solidity Interface with NatSpec & OpenZeppelin v4 Interfaces (also available at IDomainEnumerable.sol):

interface IDomainEnumerable is IDomain {
    /// @notice     Query all subdomains. Must revert if the list of domains is unknown or infinite.
    /// @return     The list of all subdomains.
    function listDomains() external view returns (string[] memory);
}

Optional Extension: Access Control

Solidity Interface with NatSpec & OpenZeppelin v4 Interfaces (also available at IDomainAccessControl.sol):

interface IDomainAccessControl is IDomain {
    /// @notice     Get if an account can move the subdomain away from the current domain
    /// @dev        May be called by `canSetDomain` of the parent domain - implement access control here!!!
    /// @param      updater The account that may be moving the subdomain
    /// @param      name The subdomain name
    /// @param      parent The parent domain
    /// @param      newSubdomain The domain that will be set next
    /// @return     Whether an account can update the subdomain
    function canMoveSubdomain(address updater, string memory name, IDomain parent, address newSubdomain) external view returns (bool);

    /// @notice     Get if an account can unset this domain as a subdomain
    /// @dev        May be called by `canDeleteDomain` of the parent domain - implement access control here!!!
    /// @param      updater The account that may or may not be able to delete a subdomain
    /// @param      name The subdomain to delete
    /// @param      parent The parent domain
    /// @return     Whether an account can delete the subdomain
    function canDeleteSubdomain(address updater, string memory name, IDomain parent) external view returns (bool);
}

Rationale

This EIP’s goal, as mentioned in the abstract, is to have a simple interface for resolving names. Here are a few design decisions and why they were made:

  • Name resolution algorithm
    • Unlike ENS’s resolution algorithm, EIP-4834’s name resolution is fully under the control of the contracts along the resolution path.
    • This behavior is more intuitive to users.
    • This behavior allows for greater flexibility - e.g. a contract that changes what it resolves to based on the time of day.
  • Parent domain access control
    • A simple “ownable” implementation was not used because this specification was designed to be as generic as possible. If an ownable implementation is desired, it can be implemented.
    • This also gives parent domains the ability to call subdomains’ access control methods so that subdomains, too, can choose whatever access control mechanism they desire
  • Subdomain access control
    • These methods are included so that subdomains aren’t always limited to their parent domain’s access control
    • The root domain can be controlled by a DAO with a non-transferable token with equal shares, a TLD can be controlled by a DAO with an EIP-20 token representing stake, a domain of that TLD can be controlled by a single owner, a subdomain of that domain can be controlled by a single owner linked to an NFT, and so on.
    • Subdomain access control functions are suggestions: an ownable domain might implement an owner override, so that perhaps subdomains might be recovered if the keys are lost.

Backwards Compatibility

There is no compatibility with ENS because ENS domains are indexed by the hash of the entire domain, while EIP-4834 domains are indexed by the subdomain name.

Reference Implementations

Some of these implementations use some contracts from OpenZeppelin for ease of understandability.

Naive Domain Implementation: NaiveDomain.sol

EIP-173 (Ownable) Domain Implementation: OwnableDomain.sol

Security Considerations

Malicious canMoveSubdomain (Black Hole)

Description

Moving a subdomain using setDomain is a dangerous operation.

Depending on the parent domain’s implementation, if a malicious new subdomain unexpectedly returns false on canMoveSubdomain, that subdomain can effectively lock the ownership of the domain.

Alternatively, it might return true when it isn’t expected (i.e. a backdoor), allowing the contract owner to take over the domain.

Mitigation

Clients should help by warning if canMoveSubdomain or canDeleteSubdomain for the new subdomain changes to false. It is important to note, however, that since these are functions, it is possible for the value to change depending on whether or not it has already been linked. It is also still possible for it to unexpectedly return true. It is therefore recommended to always audit the new subdomain’s source code before calling setDomain.

Parent Domain Resolution

Description

Parent domains have full control of name resolution for their subdomains. If a particular domain is linked to a.b.c, then b.c can, depending on its code, set a.b.c to any domain, and c can set b.c itself to any domain.

Examples

The reference Ownable domain implementation: OwnableDomain.sol

Mitigation

Before acquiring a domain that has been pre-linked, it is recommended to always have the contract and all the parents up to the root audited.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Pandapip1, "EIP-4834: Hierarchical Domain Standard [DRAFT]," Ethereum Improvement Proposals, no. 4834, February 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4834.