|
| 1 | +--- |
| 2 | +description: An universal entrypoint for resolving ENS names. |
| 3 | +contributors: |
| 4 | + - taytems.eth |
| 5 | + - raffy.eth |
| 6 | +ensip: |
| 7 | + created: "2024-10-14" |
| 8 | + status: draft |
| 9 | +--- |
| 10 | + |
| 11 | +# ENSIP-X: Universal Resolver |
| 12 | + |
| 13 | +## Abstract |
| 14 | + |
| 15 | +This ENSIP standardizes [IUniversalResolver](#specification) (UR), an universal entrypoint for resolving ENS names. UR incorporates onchain algorithms for [ENSIP-10](./10#pseudocode), [ENSIP-19](./19#algorithm), [ENSIP-21](./21), and [ENSIP-22](./22) to reduce ENS integration complexities. |
| 16 | + |
| 17 | +## Motivation |
| 18 | + |
| 19 | +The process of resolving ENS names requires multiple onchain calls and in-depth knowledge of the [latest standards](/). |
| 20 | + |
| 21 | +Resolution has become more complex over time, especially with the introduction of [wildcard resolution](./10) and [multichain primary names](./19). ENSv2 will also introduce a new registry design and many other improvements. |
| 22 | + |
| 23 | +Maintaining these changes across multiple client frameworks demands significant development effort. The growth and evolution of the ENS protocol should not be constrained by the pace of client deployments or hindered by outdated libraries. |
| 24 | + |
| 25 | +UR offers a standard entrypoint for client frameworks to perform ENS resolution. It lifts many algorithms out of client frameworks and puts them onchain for transparency and security. This ENSIP standardizes an interface for [forward](#resolve) and [primary name](#reverse) resolution. |
| 26 | + |
| 27 | +## Specification |
| 28 | + |
| 29 | +UR has the following Solidity [interface](https://github.com/ensdomains/ens-contracts/blob/20e34971fd55f9e3b3cf4a5825d52e1504d36493/contracts/universalResolver/IUniversalResolver.sol): |
| 30 | + |
| 31 | +```solidity |
| 32 | +/// @dev Interface selector: `0xcd191b34` |
| 33 | +interface IUniversalResolver { |
| 34 | + /// @notice A resolver could not be found for the supplied name. |
| 35 | + /// @dev Error selector: `0x77209fe8` |
| 36 | + error ResolverNotFound(bytes name); |
| 37 | +
|
| 38 | + /// @notice The resolver is not a contract. |
| 39 | + /// @dev Error selector: `0x1e9535f2` |
| 40 | + error ResolverNotContract(bytes name, address resolver); |
| 41 | +
|
| 42 | + /// @notice The resolver did not respond. |
| 43 | + /// @dev Error selector: `0x7b1c461b` |
| 44 | + error UnsupportedResolverProfile(bytes4 selector); |
| 45 | +
|
| 46 | + /// @notice The resolver returned an error. |
| 47 | + /// @dev Error selector: `0x95c0c752` |
| 48 | + error ResolverError(bytes errorData); |
| 49 | +
|
| 50 | + /// @notice The resolved address from reverse resolution does not match the supplied address. |
| 51 | + /// @dev Error selector: `0xef9c03ce` |
| 52 | + error ReverseAddressMismatch(string primary, bytes primaryAddress); |
| 53 | +
|
| 54 | + /// @notice An HTTP error occurred on a resolving gateway. |
| 55 | + /// @dev Error selector: `0x01800152` |
| 56 | + error HttpError(uint16 status, string message); |
| 57 | +
|
| 58 | + /// @notice Find the resolver address for `name`. |
| 59 | + /// Does not perform any validity checks on the resolver. |
| 60 | + /// @param name The name to search. |
| 61 | + /// @return resolver The found resolver, or null if not found. |
| 62 | + /// @return node The namehash of `name`. |
| 63 | + /// @return resolverOffset The offset into `name` corresponding to `resolver`. |
| 64 | + function findResolver( |
| 65 | + bytes memory name |
| 66 | + ) |
| 67 | + external |
| 68 | + view |
| 69 | + returns (address resolver, bytes32 node, uint256 resolverOffset); |
| 70 | +
|
| 71 | + /// @notice Performs ENS forward resolution for the supplied name and data. |
| 72 | + /// Caller should enable EIP-3668. |
| 73 | + /// @param name The DNS-encoded name to resolve. |
| 74 | + /// @param data The ABI-encoded resolver calldata. |
| 75 | + /// For a multicall, encode as `multicall(bytes[])`. |
| 76 | + /// @return result The ABI-encoded response for the calldata. |
| 77 | + /// For a multicall, the results are encoded as `(bytes[])`. |
| 78 | + /// @return resolver The resolver that was used to resolve the name. |
| 79 | + function resolve( |
| 80 | + bytes calldata name, |
| 81 | + bytes calldata data |
| 82 | + ) external view returns (bytes memory result, address resolver); |
| 83 | +
|
| 84 | + /// @notice Performs ENS primary name resolution for the supplied address and coin type, as specified in ENSIP-19. |
| 85 | + /// Caller should enable EIP-3668. |
| 86 | + /// @param lookupAddress The byte-encoded address to resolve. |
| 87 | + /// @param coinType The coin type of the address to resolve. |
| 88 | + /// @return primary The verified primary name, or null if not set. |
| 89 | + /// @return resolver The resolver that was used to resolve the primary name. |
| 90 | + /// @return reverseResolver The resolver that was used to resolve the reverse name. |
| 91 | + function reverse( |
| 92 | + bytes calldata lookupAddress, |
| 93 | + uint256 coinType |
| 94 | + ) |
| 95 | + external |
| 96 | + view |
| 97 | + returns ( |
| 98 | + string memory primary, |
| 99 | + address resolver, |
| 100 | + address reverseResolver |
| 101 | + ); |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +### findResolver |
| 106 | + |
| 107 | +This function performs onchain [ENSIP-1 § Registry](./#registry-specification) traversal of a DNS-encoded `name`. It returns the first non-null `resolver` address, the namehash of `name` as `node`, and the `resolverOffset` into `name` that corresponds to the resolver. If no resolver is found, `resolver` is null. |
| 108 | + |
| 109 | +`findResolver()` does not perform any validity checks on the resolver and simply returns the value in the registry. The resolver may not be a contract or a resolver. |
| 110 | + |
| 111 | +#### <a name="resolve-example">Pseudocode Example</a> |
| 112 | + |
| 113 | +```js |
| 114 | +name = dnsEncode("sub.nick.eth") = "\x03sub\x04nick\x03eth\x00" |
| 115 | + |
| 116 | +1. registry[namehash("\x03sub\x04nick\x03eth\x00")] = null ❌️ |
| 117 | +2. registry[namehash(/*-4-*/"\x04nick\x03eth\x00")] = 0x2222222222222222222222222222222222222222 ✅️ // "nick.eth" |
| 118 | +3. registry[namehash(/*-----9-----*/"\x03eth\x00")] = ... // not |
| 119 | +4. registry[namehash(/*---------13-------*/"\x00")] = ... // checked |
| 120 | + |
| 121 | +findResolver(name) |
| 122 | + resolver = registry[namehash("\x04nick\x03eth\x00")] = 0x2222222222222222222222222222222222222222 |
| 123 | + node = namehash("\x03sub\x04nick\x03eth\x00") = 0xe3d81fd7b7e26b124642b4f160ea05f65a28ecfac48ab767c02530f7865e1c4c |
| 124 | + offset = 4 // name.slice(4) = "\x04nick\x03eth\x00" = dnsEncode("nick.eth") |
| 125 | +``` |
| 126 | + |
| 127 | +### resolve |
| 128 | + |
| 129 | +This function performs ENS forward resolution using the `resolver` found by [`findResolver()`](#findresolver). It provides a standard interface for interacting [ENSIP-1](./1) and [ENSIP-10](./10) resolvers for onchain and offchain resolution. Provided a DNS-encoded `name` and ABI-encoded `data`, it returns the ABI-encoded resolver `result` and the valid `resolver` address. |
| 130 | + |
| 131 | +UR automatically handles wrapping calldata and unwrapping responses when interacting with an [`IExtendedResolver`](./10#pseudocode) and safely interacts with contracts deployed before [EIP-140](https://eips.ethereum.org/EIPS/eip-140). |
| 132 | + |
| 133 | +##### <a name="resolve-resolution-errors">Resolution Errors</a> |
| 134 | + |
| 135 | +* If no resolver was found, reverts `ResolverNotFound`. |
| 136 | +* If the resolver was not a contract, reverts `ResolverNotContract`. |
| 137 | +* If [EIP-3668](https://eips.ethereum.org/EIPS/eip-3668) (CCIP-Read) was required and it was not handled by the client, reverts `OffchainLookup`. |
| 138 | +* If CCIP-Read was handled but the `OffchainLookup` failed, reverts `HTTPError`. |
| 139 | + |
| 140 | +##### <a name="resolve-resolver-errors">Resolver Errors</a> |
| 141 | + |
| 142 | +* If the called function was not implemented, reverts `UnsupportedResolverProfile`. |
| 143 | +* If the called function reverted, reverts `ResolverError`. |
| 144 | + |
| 145 | +#### Smart Multicall |
| 146 | + |
| 147 | +Traditionally, resolvers have been written to answer direct profile requests, eg. `addr()` returns one address. To perform multiple requests, the caller must perform multiple independent requests (in sequence, parallel, or via batched RPC) or utilize an [external multicall contract](https://www.multicall3.com/) which does not support CCIP-Read. |
| 148 | + |
| 149 | +UR supports multicall with CCIP-Read and [`eth.ens.resolver.extended.multicall`](./22) feature. To perform multiple calls: |
| 150 | +```solidity |
| 151 | +bytes[] memory calls = new bytes[](3); |
| 152 | +calls[0] = abi.encodeCall(IAddrResolver.addr, (node)); |
| 153 | +calls[1] = abi.encodeCall(ITextResolver.text, (node, "avatar")); |
| 154 | +calls[2] = hex"00000000"; // invalid selector |
| 155 | +``` |
| 156 | +```ts |
| 157 | +const calls = [ |
| 158 | + encodeFunctionData({ functionName: "addr", args: [node] }), |
| 159 | + encodeFunctionData({ functionName: "text", args: [node, "avatar"] }), |
| 160 | + "0x00000000", // invalid selector |
| 161 | +]; |
| 162 | +``` |
| 163 | +Using the following [interface](https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/IMulticallable.sol): |
| 164 | +```solidity |
| 165 | +interface IMulticallable { |
| 166 | + function multicall(bytes[] calldata data) external view returns (bytes[] memory); |
| 167 | +} |
| 168 | +``` |
| 169 | +Encode the calls, invoke [`resolve()`](#resolve) normally, and decode the result: |
| 170 | +```solidity |
| 171 | +bytes memory data = abi.encodeCall(IMulticallable.multicall, (calls)); |
| 172 | +(bytes memory result, address resolver) = UR.resolve(name, data); // note: could revert OffchainLookup |
| 173 | +bytes[] memory results = abi.decode(result, (bytes)); |
| 174 | +``` |
| 175 | +```ts |
| 176 | +const data = encodeFunctionData({ functionName: "multicall", args: [calls] }); |
| 177 | +const [result, resolver] = await UR.read.resolve(name, data); |
| 178 | +const results = decodeFunctionResult({ functionName: "multicall", data: result }); |
| 179 | +``` |
| 180 | +The same [resolution errors](#resolve-resolution-errors) apply but [resolver errors](#resolve-resolver-errors) are handled differently. The call **always succeeds** and decodes into an array of results. The number of calls is always equal to the number of results. If `results[i]` is not multiple of 32 bytes, it is an ABI-encoded error for the corresponding `calls[i]`. |
| 181 | + |
| 182 | +```solidity |
| 183 | +address ethAddress = abi.decode(results[0], (address)); |
| 184 | +string avatar = abi.decode(results[1], (string)); |
| 185 | +// results[2] == abi.encodeWithSelector(UnsupportedResolverProfile.selector, bytes4(0x00000000)); |
| 186 | +``` |
| 187 | +```ts |
| 188 | +const ethAddress = decodeFunctionResult({ functionName: "addr", data: results[0] }); |
| 189 | +const avatar = decodeFunctionResult({ functionName: "text", data: results[1] }); |
| 190 | +const error = decodeErrorResult({ data: result[2] }); // { errorName: "UnsupportedResolverProfile", args: ["0x00000000"] } |
| 191 | +``` |
| 192 | + |
| 193 | +### reverse |
| 194 | + |
| 195 | +This function performs ENS primary name resolution according to [ENSIP-19](./19). Provided a [byte-encoded](./9) `lookupAddress` and desired `coinType`, it returns the verified primary `name` and the addresses of forward `resolver` and `reverseResolver`. UR supports CCIP-Read during the forward and reverse phases. |
| 196 | + |
| 197 | +If the primary `name` is [unnormalized](./15), eg. `normalize("Nick.eth") != "nick.eth"`, then `name` and `resolver` are invalid. |
| 198 | + |
| 199 | +* If [reverse resolution](./19#reverse-resolution) of the reverse name was not successful, reverts a [`resolve()` error](resolve-resolution-errors). |
| 200 | +* If the resolved primary name was null, returns `("", address(0), <reverseResolver>)`. |
| 201 | +* If [forward resolution](./19#forward-resolution) of the primary name was not successful, also reverts a [`resolve()` error](resolve-resolution-errors). |
| 202 | +* If the resolved address of `coinType` doesn't equal the `lookupAddress`, reverts `ReverseAddressMismatch`. |
| 203 | + |
| 204 | +`reverse()` is effectively (2) sequential `resolve()` calls. |
| 205 | + |
| 206 | +#### <a name="reverse-example">Pseudocode Example</a> |
| 207 | + |
| 208 | +```js |
| 209 | +// valid primary name: vitalik.eth on mainnet |
| 210 | +reverse("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 60) |
| 211 | + 1. reverse: resolve("d8da6bf26964af9d7eed9e03e53415d37aa96045.addr.reverse", name()) = "vitalik.eth" |
| 212 | + 2. forward: resolve("vitalik.eth", addr(60)) = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 |
| 213 | + 3. 0xd8dA == 0xd8dA => ✅️ "vitalik.eth" |
| 214 | + |
| 215 | +// invalid primary name: imposter vitalik.eth |
| 216 | +reverse("0x314159265dD8dbb310642f98f50C066173C1259b", 60) |
| 217 | + 1. reverse: resolve("314159265dd8dbb310642f98f50c066173c1259b.addr.reverse", name()) = "vitalik.eth" |
| 218 | + 2. forward: resolve("vitalik.eth", addr(60)) = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 |
| 219 | + 3. 0x3141 != 0xd8d => ❌️ ReverseAddressMismatch() |
| 220 | + |
| 221 | +// no primary name: burn address on Base mainnet |
| 222 | +resolve("0x000000000000000000000000000000000000dEaD", 0x80000000 ^ 8453) |
| 223 | + 1. reverse: resolve("000000000000000000000000000000000000dead.addr.reverse", name()) => ❌️ ResolverNotFound() |
| 224 | +``` |
| 225 | + |
| 226 | +## Backwards Compatibility |
| 227 | + |
| 228 | +UR supports **ALL** known resolver types if the caller supports CCIP-Read. Otherwise, it can only resolve onchain names. |
| 229 | + |
| 230 | +It is a **complete replacement** for existing ENS resolution procedures. Client frameworks should focus on building calldata and handling responses and rely on UR to facilitate resolution. |
| 231 | + |
| 232 | +## Security Considerations |
| 233 | + |
| 234 | +UR uses a batch gateway to perform CCIP-Read requests. If the client does not support [ENSIP-21](./21), a trustless external batch gateway service is used which adds latency and leaks information. |
| 235 | + |
| 236 | +UR is deployed as an immutable contract and as an ENS DAO-managed upgradeable proxy. The main purpose of the proxy is to facilitate a seamless transition to [ENS v2](https://ens.domains/ensv2) and track the latest standards. Client frameworks should default to the proxy so their libraries are future-proof, with the option to specify an alternative implementation. |
| 237 | + |
| 238 | +## Copyright |
| 239 | + |
| 240 | +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). |
0 commit comments