Skip to content

Commit 976e781

Browse files
authored
Add ENSIP-23: Universal Resolver (ensdomains#11)
1 parent a39ed2b commit 976e781

File tree

1 file changed

+240
-0
lines changed

1 file changed

+240
-0
lines changed

ensips/23.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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

Comments
 (0)