Alert Source Discuss
Standards Track: ERC

ERC-4834: 分层域名

极其通用的名称解析

Authors Gavin John (@Pandapip1)
Created 2022-02-22

摘要

这是一个用于通用名称解析的标准,具有任意复杂的访问控制和解析。它允许实现此 EIP 的合约(以下称为“域”)可以使用更友好的名称进行寻址,其目的与 ERC-137(也称为“ENS”)类似。

动机

此 EIP 优于现有标准的优势在于,它提供了一个支持名称解析的最小接口,添加了标准化访问控制,并且具有简单的架构。ENS 虽然有用,但其架构相对复杂,并且没有标准访问控制。

此外,所有域(包括子域、TLD 甚至根域本身)实际上都是作为域来实现的,这意味着名称解析是一个简单的迭代算法,与 DNS 本身非常相似。

规范

本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”应按照 RFC 2119 中的描述进行解释。

合约接口

interface IDomain {
    /// @notice     查询域是否具有具有给定名称的子域
    /// @param      name 要查询的子域,从右到左的顺序
    /// @return     如果域具有具有给定名称的子域,则为 `true`,否则为 `false`
    function hasDomain(string[] memory name) external view returns (bool);

    /// @notice     获取具有给定名称的子域
    /// @dev        如果 `hasDomain(name)` 为 `false`,则应恢复
    /// @param      name 要获取的子域,从右到左的顺序
    /// @return     具有给定名称的子域
    function getDomain(string[] memory name) external view returns (address);
}

名称解析

要解析名称(如 "a.b.c"),请按分隔符将其拆分(产生类似于 ["a", "b", "c"] 的结果)。最初将 domain 设置为根域,并将 path 设置为空列表。

弹出数组的最后一个元素 ("c") 并将其添加到路径,然后调用 domain.hasDomain(path)。如果它是 false,则域名解析失败。否则,将域设置为 domain.getDomain(path)。重复此操作,直到拆分段的列表为空。

可能的嵌套数量没有限制。例如,如果根包含 z,并且 z 包含 y,依此类推,则 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 将是有效的。

这是一个解析名称的 solidity 函数:

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

可选扩展:可注册

interface IDomainRegisterable is IDomain {
    //// Events
    
    /// @notice     当创建新的子域时必须发出(例如,通过 `createDomain`)
    /// @param      sender createDomain 的 msg.sender
    /// @param      name createDomain 的名称
    /// @param      subdomain createDomain 中的子域
    event SubdomainCreate(address indexed sender, string name, address subdomain);

    /// @notice     当域的已解析地址更改时必须发出(例如,使用 `setDomain`)
    /// @param      sender setDomain 的 msg.sender
    /// @param      name setDomain 的名称
    /// @param      subdomain setDomain 中的子域
    /// @param      oldSubdomain 旧子域
    event SubdomainUpdate(address indexed sender, string name, address subdomain, address oldSubdomain);

    /// @notice     当域被取消映射时必须发出(例如,使用 `deleteDomain`)
    /// @param      sender deleteDomain 的 msg.sender
    /// @param      name deleteDomain 的名称
    /// @param      subdomain 旧子域
    event SubdomainDelete(address indexed sender, string name, address subdomain);

    //// CRUD
    
    /// @notice     使用给定的名称创建子域
    /// @dev        如果 `canCreateDomain(msg.sender, name, pointer)` 为 `false` 或域存在,则应恢复
    /// @param      name 要创建的子域名
    /// @param      subdomain 要创建的子域
    function createDomain(string memory name, address subdomain) external payable;

    /// @notice     使用给定的名称更新子域
    /// @dev        如果 `canSetDomain(msg.sender, name, pointer)` 为 `false` 或域不存在,则应恢复
    /// @param      name 要更新的子域名
    /// @param      subdomain 要设置的子域
    function setDomain(string memory name, address subdomain) external;

    /// @notice     删除具有给定名称的子域
    /// @dev        如果域不存在或 `canDeleteDomain(msg.sender, name)` 为 `false`,则应恢复
    /// @param      name 要删除的子域
    function deleteDomain(string memory name) external;


    //// 父域访问控制

    /// @notice     获取帐户是否可以使用给定的名称创建子域
    /// @dev        如果 `hasDomain(name)` 为 `true`,则必须返回 `false`。
    /// @param      updater 可能能够或可能无法创建/更新子域的帐户
    /// @param      name 将被创建/更新的子域名
    /// @param      subdomain 将被设置的子域
    /// @return     帐户是否可以更新或创建子域
    function canCreateDomain(address updater, string memory name, address subdomain) external view returns (bool);

    /// @notice     获取帐户是否可以更新或创建具有给定名称的子域
    /// @dev        如果 `hasDomain(name)` 为 `false`,则必须返回 `false`。
    ///             如果 `getDomain(name)` 也是实现子域访问控制扩展的域,如果 `getDomain(name).canMoveSubdomain(msg.sender, this, subdomain)` 为 `false`,则应返回 `false`。
    /// @param      updater 可能能够或可能无法创建/更新子域的帐户
    /// @param      name 将被创建/更新的子域名
    /// @param      subdomain 将被设置的子域
    /// @return     帐户是否可以更新或创建子域
    function canSetDomain(address updater, string memory name, address subdomain) external view returns (bool);

    /// @notice     获取帐户是否可以删除具有给定名称的子域
    /// @dev        如果 `hasDomain(name)` 为 `false`,则必须返回 `false`。
    ///             如果 `getDomain(name)` 是实现子域访问控制扩展的域,如果 `getDomain(name).canDeleteSubdomain(msg.sender, this, subdomain)` 为 `false`,则应返回 `false`。
    /// @param      updater 可能能够或可能无法删除子域的帐户
    /// @param      name 要删除的子域
    /// @return     帐户是否可以删除子域
    function canDeleteDomain(address updater, string memory name) external view returns (bool);
}

可选扩展:可枚举

interface IDomainEnumerable is IDomain {
    /// @notice     查询所有子域。如果域的数量未知或无限,则必须恢复。
    /// @return     具有给定索引的子域。
    function subdomainByIndex(uint256 index) external view returns (string memory);
    
    /// @notice     获取子域的总数。如果域的数量未知或无限,则必须恢复。
    /// @return     子域的总数。
    function totalSubdomains() external view returns (uint256);
}

可选扩展:访问控制

interface IDomainAccessControl is IDomain {
    /// @notice     获取帐户是否可以将子域从当前域移开
    /// @dev        可能由父域的 `canSetDomain` 调用 - 在此处实现访问控制!!!
    /// @param      updater 可能正在移动子域的帐户
    /// @param      name 子域名
    /// @param      parent 父域
    /// @param      newSubdomain 接下来要设置的域
    /// @return     帐户是否可以更新子域
    function canMoveSubdomain(address updater, string memory name, IDomain parent, address newSubdomain) external view returns (bool);

    /// @notice     获取帐户是否可以取消设置此域作为子域
    /// @dev        可能由父域的 `canDeleteDomain` 调用 - 在此处实现访问控制!!!
    /// @param      updater 可能能够或可能无法删除子域的帐户。
    /// @param      name 要删除的子域
    /// @param      parent 父域
    /// @return     帐户是否可以删除子域
    function canDeleteSubdomain(address updater, string memory name, IDomain parent) external view returns (bool);
}

理由

如摘要中所述,此 EIP 的目标是拥有一个用于解析名称的简单接口。以下是一些设计决策以及做出这些决策的原因:

  • 名称解析算法
    • 与 ENS 的解析算法不同,此 EIP 的名称解析完全由解析路径上的合约控制。
    • 这种行为对用户来说更直观。
    • 这种行为允许更大的灵活性 - 例如,一个根据一天中的时间更改其解析内容的合约。
  • 父域访问控制
    • 没有使用简单的“可拥有”接口,因为此规范旨在尽可能通用。如果需要可拥有实现,则可以实现它。
    • 这也使父域能够调用子域的访问控制方法,以便子域也可以选择他们想要的任何访问控制机制
  • 子域访问控制
    • 包含这些方法是为了使子域不总是受限于其父域的访问控制
    • 根域可以由具有具有相等份额的不可转让令牌的 DAO 控制,TLD 可以由具有表示股份的令牌的 DAO 控制,该 TLD 的域可以由单个所有者控制,该域的子域可以由链接到 NFT 的单个所有者控制,依此类推。
    • 子域访问控制功能是建议:可拥有的域可能会实现所有者覆盖,因此如果密钥丢失,可能会恢复子域。

向后兼容性

此 EIP 足够通用以支持 ENS,但 ENS 不够通用以支持此 EIP。

安全考虑

恶意 canMoveSubdomain(黑洞)

描述:恶意 canMoveSubdomain

使用 setDomain 移动子域是一种潜在的危险操作。

根据父域的实现,如果恶意的新的子域意外地在 canMoveSubdomain 上返回 false,则该子域可以有效地锁定域的所有权。

或者,它可能会在不希望的时候返回 true(即后门),允许合约所有者接管该域。

缓解措施:恶意 canMoveSubdomain

如果新子域的 canMoveSubdomaincanDeleteSubdomain 更改为 false,客户端应通过发出警告来提供帮助。但重要的是要注意,由于这些是函数,因此该值可能会根据是否已链接而变化。它仍然有可能意外地返回 true。因此,建议在调用 setDomain 之前始终审计新子域的源代码。

父域解析

描述:父域解析

父域完全控制其子域的名称解析。如果一个特定的域名链接到 a.b.c,那么 b.c 可以根据其代码,将 a.b.c 设置为任何域名,而 c 可以将 b.c 本身设置为任何域名。

缓解措施:父域解析

在获取已预先链接的域之前,建议始终审核合约以及直到根目录的所有父目录。

版权

CC0 下放弃版权和相关权利。

Citation

Please cite this document as:

Gavin John (@Pandapip1), "ERC-4834: 分层域名," Ethereum Improvement Proposals, no. 4834, February 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4834.