Reverse Resolution in off-chain settings

(A) can be served by the use of default reverse/primary name so I am not sure why you think it’s only useful for (B) (this fallback option is actually proposed by @Premm.eth). As I also mentioned in the modified post above, the ability to specify chain specific primary name is more of a security requirement and security does come as a highest priority in crypto imo

Agreed not to compromise on security. Hopefully there’s a path that doesn’t compromise security while also not compromising on the value propositions of ENS or on ENS market adoption.

That’s basically the role of the default primary name. Contract accounts cannot set the default primary name because it only accepts signature which only EOA can generate.

As I repeated, you don’t have to set chain specific name if you only set the default primary name.

The ability to specify chain specific address has been discussed in other places. The ability to specify which chain to accept payment/transactions is actually a useful UX feature to give assurance to the end users that recipients won’t lose their funding by sending assets to a wrong chain.

There’s a lot of words here and I’m not sure I understand, so I’m just gonna say things:


Reverse resolution {addr}.addr.reverse is just addressname.

I see no purpose in having anything more complicated than this, so I’m against the current subdomain → resolver = PR → PR.setName() setup simply for gas and complexity.

For example, do we really need this? raffy.51050ec063d393217b436747617ad1c2285aeeee.addr.reverse
Note: I think there’s value in having EoA-claimable nodes in the registry, but they’re massive overkill for what reverse names do for 99.99999% of use-cases.

The fact that contracts aren’t widely named in 2024 is also odd to me.


A much simpler registry would just store reverse-key (node or address)resolver address outside of the ENS tree — aka 1 slot on-chain. This registry would only be editable through DAO approved registrars using different proving mechanisms. These should be trustless and irrevocable.

*.addr.reverse (or *.reverse) can be wildcard, and it consults this simplified registry, and forwards the request to the resolver set for that address.

  1. There should be a simple storage contract L (literal) that stores reverse-keyname (string) and any EOA who wants string-based on-chain names can use that.

  2. There should be another contract W (wrapper) that has a map reverse-keynode which converts your reverse address namehash to a wrapped namehash, and then just calls names(), eliminating the need to store a name because it’s already stored on-chain.

  3. If you want a different proving mechanism, deploy another contract (eg. signature, contract ownership, etc.). For example, why can’t contracts just be asked for their own primary? For example, if a contract sets their reverse-keyF (contract forward), F can be a contract that simply calls C.primaryName() when it’s asked to F.resolve(C.addr.reverse, name()).

  4. If you set your reverse-keycustom resolver, then that contract can do whatever, and go off-chain.

If we’re considering using CCIP for reverse resolution, which requires client changes, we should use a solution like this, as it gives maximum control to the owner and minimizes gas.


Some examples:

  • If I have 100 EoAs, I should be able to deploy a contract Q that answers resolve(*.reverse.addr, name()) for any of those 100 addresses, and responds "raffy.eth". And then for each of those 100 EoAs, set reverse keyQ. I should also be able to provide some kind of delegated signature, so I can do this in 1 transaction.

  • If I deploy a new contract, I should be able implement a function primaryName() returns (string) that is my primary name. During the constructor, I claim that name by finding that registrar by name, and then calling claim() on it. That should cost almost nothing (eg. set resolver keyF (forwarding contract above).

  • For virtual names of basename B, EoAs should be able to set reverse-keyB and then that wildcard contract (likely off-chain) can provide the primary.

Additionally, name(node) could be generalized to name(node, chain).

A related idea from Expanding Beyond Mainnet was having a special coinType which indicates universal EVM deployment address (maybe 0x8000003c, evm bit + 60).

Thank you everyone for the rich and enlightening discussion on this topic. The insights shared have greatly contributed to evolving my understanding and perspective regarding the implementation of our proposal.

Firstly, I apologize for the unconventional format of my initial proposal. Instead of a traditional, well-structured proposal, I opted for a more illustrative, step-by-step guide. I realize now the importance of summarizing our concept more succinctly and cohesively:

At the core, our proposal aims to enable off-chain subnames to set primary names, facilitating reverse resolution. Our goal was to devise a solution that would seamlessly integrate without disrupting existing implementations while also optimizing the gas costs for users. Our primary focus was on off-chain mechanisms, although, with hindsight, I acknowledge the potential for extending this to include L2 chains through a more generalized approach.

The critical component for off-chain solutions is establishing an on-chain pointer. This pointer would direct the reverse resolution process to a customer resolver being utilized for off-chain resolutions. Our proposed custom resolver is relatively straightforward, featuring a resolve function capable of enabling address reverse resolution via CCIP-Read (similar to the forward resolution flow). However, incorporating @raffy ’s suggestion for a “delegated signature mechanism” could further enhance efficiency, although it would necessitate additional modifications.

Thank you @matoken.eth for sharing the EVM reverse resolution proposal, I really appreciate the approach you are taking by setting a generalized solution for L2s, “without resolver customisability”, and I completely agree with the way you approached this to account for smart contract accounts and not only EOAs. But I also agree with @lightwalker.eth that “ENS onboarding is incomplete if only forward resolution is configured. A complete ENS onboarding should include configuring a primary name that isn’t “sandboxed” to a particular app or chain” for off-chain names.

I am more than happy to continue working on this with you guys, but I am unsure of future steps. For example, I don’t know if it would be better to have two separate proposals, one being the EVM reverse resolution which is accounting for L2 chains (along with contract accounts) and one for off-chains solutions, or merge them both in a generalized big proposal, I am unsure of how feasible this would be, given the divergence between the two proposals.

1 Like

As a quick demo, I set the TOR as my reverse resolver and set the context to use this demo which returns the requesting IP address + the current time as the reverse name.

My resolver already supports CCIP everywhere so this just works:

1 Like

Exactly! I think we’re aligned on the flow, the only difference is that I was using a custom resolver instead of the TOR (btw cool name), but it should be pretty straightforward moving the implementation towards a generalised resolver, as it makes sense…

I think I had missed this part when first reading the replies to the thread. This is a really interesting alternative, instead of having the user perform a one time transaction. Could you please elaborate on how this would work?

In general, I think the only feature we need from reverse resolution is addressresolver, as we must store at least one slot on-chain (as no default is possible) so why not just store a resolver, and call resolver.name(address) view returns (string) with CCIP-Read enabled.

However, setSubnodeRecord is sufficient. I wonder if it’s cheaper if you call it twice in the same tx and zero everything but the resolver.


I think the only necessary change (which I personally think is the current standard) is that ENSIP-10 resolution should apply universally — so if the reverse resolver is wildcard, you must call resolve(name()).

If this is a change, might as well make it: name(node, chain) returns (string)

With this clarification in middleware, ENS could offer free? offchain reverse at signup by setting their resolver and storing an off-chain record. Obviously, they would be encouraged to set an on-chain record, but it doesn’t have to be right away.

1 Like

It would be interesting to see if setSubnodeRecord is indeed cheaper. I was experimenting, and I didn’t try it. This was the cheapest flow I had found

Call the function recordExists() on the ENS Registry contract, the next steps would depend on its response:

  • True: Call ["function setResolver(bytes32 node, address resolver)”] on ENS Registry contract and pass the node and the off-chain resolver address. Costs 31,165 gas units.
  • False: Call [”function claimWithResolver(address owner, address resolver)”] on Reverse Registrar Contract and pass the address along with the off-chain resolver address. Costs 44,002 gas units and 81,002 gas units for the first time.

I’d have to review the contract to be sure, but setting just the resolver would be sufficient (ENSIP-10 doesn’t check owner), but there’s no setSubnodeResolver() so you must call setSubnodeRecord() (setOwner + setResolver) but the parent could zero owner before returning using a different registrar.

I’m not sure what it is you’re actually proposing; your first post reads like a description of how things work today, to me. What is it that you think should be changed?

It was intended as a proposal to standardize the flow for setting a primary name for subnames, not a proposal for changes

It does look like you can save some gas using this technique.

I deployed a BudgetReverseRegistrar and then set a resolver from an EOA, where:

function claim(address resolver) external {
    bytes32 hash = sha3HexAddress(msg.sender);
    ens.setSubnodeRecord(REVERSE_NODE, hash, address(this), resolver, 0);
    ens.setSubnodeOwner(REVERSE_NODE, hash, address(0));
}

This is just exploiting the fact that the owner of a reverse record is superfluous as the reverse registrar lets the rightful owner reclaim it.

If that resolver was an wildcard off-chain resolver, an external server would just answer resolve("{addr}.addr.reverse", name()) which is addressname.

85k gas for any length name (since no name stored on-chain)

The current reverse registrar for short names is 114k gas (34% more expensive) and 160k gas for a 33-character primary.

IMO, setting a wildcard on addr.reverse seems dangerous since it lets that resolver provide a name for any address, but individually setting each address’s resolver to the same wildcard seems fine. Minimizing gas to set your reverse resolver to an offchain database seems reasonable for on-boarding.


Like I described previously, using the same setup, new contracts could set their resolver to themselves during construction, and then just answer their own name(node) query.

Or set their resolver to some fixed deployment, like DotEthContractNameWildcardResolver, which on resolve("{addr}.addr.reverse", name()), reads addr.dotethname() from the corresponding contract and appends ".eth".

I believe Viem uses the UR but ethers was using non-ENSIP-10 resolution for lookupAddress(). I submit a PR to fix this.

1 Like

I guess this is a reasonable price for the user, as it is also optional. Setting the primary name would cost him ≈ 80k during onboarding and ≈40k if he already had interacted with ens previously (as he would only need to set the resolver address).

This is great, thank you. Feel free to use this library until the PR is merged. After that, you can switch back to ethers since address resolution is the only feature it supports.

Namesys offchain resolver “ccip2.eth” it’s fully compatible with reverse name as we use ../.well-known/eth/domain/sub/.. format, so for reverse records it can use ../.well-known/reverse/addr/<address> as records storage… but for simplicity we prefer to use default on-chain resolver coz off-chain setup requires new reverse domain/storage costing extra gas/txs, and users are most likely not going to update their reverse records more frequently per address.

If you are using coinType, it won’t be usable for new L2 chains or development chains especially OP Stack / Arbitrum Orbit chains that are yet to have their own token or are not planned to have their own token.

coinType is defined by slip44 which is not related to the existence of coins.
For EVM chain, we have ENSIP to convert evm chain id to the cointype

There are concerns that come to mind regarding the deployment and integration of the L2ReverseRegistrar:

  • Is the L2ReverseRegistrar deployed deterministically, allowing the deployment address to be predicted from the coinType?
  • If not, integrating this feature into SDKs and wallets becomes challenging, as addresses must be manually input for each chain, and there are thousands of chains.
  • An example of this issue is the UniversalResolver, whose address changes with each upgrade, leading to SDKs not following new upgrades and continuing to use an older version.
  • Can the L2ReverseRegistrar be deployed on a new chain without obtaining permission from ENS and still have integration with SDKs and wallets?

A live example of an obsolete UniversalResolver problem is found in Viem and I have just opened a pull request to upgrade the UniversalResolver.