📖 本 EIP 正处于审查阶段。它可能会有变化,希望得到反馈。

EIP-4834: Hierarchical Domains Source

Extremely generic name resolution

作者 Pandapip1
讨论-To https://ethereum-magicians.org/t/erc-4834-hierarchical-domains-standard/8388
状态 Review
类型 Standards Track
分类 ERC
创建日期 2022-02-22
依赖 137, 165
英文版 https://eips.ethereum.org/EIPS/eip-4834

Abstract

This is a standard for generic name resolution with access control. It permits a contract that implements this EIP (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).

Any program that resolves domains should treat domains as equivalent to their resolved addresses. In practice, this means users of DApps that implement this EIP’s 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, this EIP takes a different approach, and permits any access control patterns to be implemented.

Motivation

The advantage of this EIP over 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 and does not have standard access control.

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, not unlike DNS itself.

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 (e.g. 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 (e.g. 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 (e.g. 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)` 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 implementing the subdomain access control extension, 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 implementing the subdomain access control extension, 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);
}

As per EIP-165, supportsInterface(0xe3ffd947) MUST return true.

Name Resolution

To resolve a name (like "a.b.c"), 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 number of domains is unknown or infinite.
    /// @return     The subdomain with the given index.
    function subdomainByIndex(uint256 index) external view returns (string memory);

    /// @notice     Get the total number of subdomains. Must revert if the number of domains is unknown or infinite.
    /// @return     The total number of subdomains.
    function totalSubdomains() external view returns (uint256);
}

As per EIP-165, supportsInterface(0x8d5fd78e) MUST return true if IDomainEnumerable is used.

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);
}

As per EIP-165, supportsInterface(0x1b2e22d2) MUST return true if IDomainAccessControl is used.

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, this EIP’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” interface 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 a 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.

向后兼容性

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

Reference Implementation

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

Ownable Domain Implementation: OwnableDomain.sol

Security Considerations

Malicious canMoveSubdomain (Black Hole)

Description: Malicious canMoveSubdomain

Moving a subdomain using setDomain is a potentially 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: Malicious canMoveSubdomain

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 Domain Resolution

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: Parent Domain Resolution

The reference Ownable domain implementation: OwnableDomain.sol

Mitigation: Parent Domain Resolution

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.

参考文献

Please cite this document as:

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