Determine resolver functionalities using named contracts w/ ABI resolver

Context

There has been a proliferation of cross-chain subdomain services and the reliance on CCIP-read to achieve resolution. Today, when a resolution request fails to return data, it isn’t clear whether this is due to lack of resolver support, lack of name data or some other error.

@raffy has attempted to reconcile this by way of a draft ENSIP which uses new custom errors emitted by resolvers for each failure class. However, this approach requires redeploying contracts to include these new errors AND relies on a new error code type to be returned in the callback to the originating L1 contract for parsing and handling.

Although this solution would work, the added complexity and operational burden would likely mean slow adoption by existing subdomain services and lengthy migration periods where users are still relying on outdated resolvers.

Error types

There are two explicit resolver errors suggested in the draft ENSIP for custom ESNIP-10 errors.

  1. UnsupportedResolverProfile for signaling that a resolver does not support a specific resolver profile.
  2. UnreachableName for signaling that the name does not exist, cannot be parsed, or is otherwise incorrectly encoded.

Alternative Approach

There is an ABI Resolver profile included in a majority of existing public resolvers which allows a named contract to store its abi data. By naming resolver contracts and then associating the resolver ABI data with the resolver’s name, clients and gateways have an avenue for checking the functionalities of a resolver before querying it. This completely solves the class of errors that precipitated the need for UnsupportedResolverProfile. By eliminating one of the two error classes, we can then implicitly throw UnreachableName if the queried resolver does support the expected profile.

The implementation of this in practice requires a few changes:

  1. That resolvers are consistently named and assigned ABI data
  2. That the generic gateway and clients that do not use the Universal Resolver are enabled to query for this ABI data and then ingest it to determine whether an UnsupportedResolverProfile error should be thrown
  3. That the UniversalResolver is upgraded to consume this error from the Gateway/Client

Seeking feedback

  • Does this need to be an ENSIP requiring that resolvers are assigned names + abi resolver data?
  • Am I missing any high level implementation cases?
  • Any reasons to avoid this solution?

Interesting proposal! Two potential issues:

  1. A resolver that supports IExtendedResolver is not obliged to implement the legacy resolver methods for all the resolver profiles it supports. Thus, you can’t necessarily determine all the supported profiles by examining the ABI.
  2. It’s unclear to me that requiring this change of all resolvers would be easier than instead deploying them with the new error values.

As an experiment, I explored the idea of adding “external” resolver feature detection, which is a cousin of katzman’s suggestion about using the ABIResolver.

“External” feature detection basically means that an IExtendedResolver (since they don’t directly implement the resolver profiles and only implement resolve()) can define its feature somewhere else. The only assumption is that this information MUST be onchain (CCIP-Read is disabled.)

The original idea was: if resolver R is IExtendedResolver, resolve {R}.addr.reverse (mainnet only), check its forward address, and then ask the forward resolver for a feature using ABIResolver.

Since ABIResolver requires some parsing (contentType + ABI decoding), I considered using InterfaceResolver. However, that also isn’t exactly what we want, so I considered AddressResolver with an exotic coin type C and either one coin type per feature (eg. C = keccak256("resolve(multicall")) for “supports multicall”) or a single coin type with a bitfield of features (eg. C = keccak256("ensip10.features") with 1 << 0 for “supports multicall”).

The immediate problem using the forward resolver is that the reverse() flow also invokes feature detection (because the forward check makes 2 addr() calls) which requires reverse() and causes an infinite loop. So I changed the location of the feature storage to the reverse resolver instead.

A further improvement would be, if there is no name() defined, ask the resolver itself (since it’s IExtendedResolver, this would be resolve(*) for whatever detection method is used (eg. resolve(addr(C)) != 0).


I created an experimental PR which implements feature detection of resolve(multicall) just to see how it could work:

  • _supportsFeature() asks the reverse resolver of the current resolver (eg. must be named) for a feature using addr(C) != 0 (one of the ideas from above)
    • it would be simple to consider the resolver itself when there is no name()
    • it would be simple to change this to a bitfield (or any other resolver profile)
  • _resolveBatch() performs resolve(multicall) feature detection if IExtendedResolver and 2+ calls, and then bypasses the batch gateway and performs a single direct call.
  • this test enables the feature on the reverse resolver and then does UR.resolve() with 2+ calls, that results in a single resolve(multicall) w/o batch gateway assistance.

I agree this is a bit weird and Nick’s claim #2 is likely true, but I think the general idea of using the reverse resolver for additional information (since the registry only supports resolver/owner/ttl) is interesting.

I think supportsInterface() for “supports resolve(multicall)” is definitely a good solution, just requires a standard selector to check.

Or, if resolve() followed the ENSIP above, we could simply try resolve(multicall()) (with CCIP-Read disabled) and see if it reverts UnsupportedResolverProfile() (although this doesn’t necessarily mean its supported since some resolvers accept any input.) Expecting an immediate return from resolve(multicall([])) is also a viable approach (but require a more complex resolver implementation.)


It’s unclear how many features there would actually be?

  1. supports resolve(multicall)
  2. wrappable / recursive CCIP-Read safe
    (We’ve been floating the idea of keccak256("eip3368.wrappable") for “recursive CCIP-Read safe” but this feature might be unnecessary if we can get non-compliant resolvers/gateways to upgrade.)
  3. is an crosschain gateway (ie. Unruggable)