EIP-4834: Hierarchical Domains
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
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.