New standard proposal: ENS multicoin support

I’ve written up a draft EIP proposing a new resolver field, addresses, to support resolution of addresses for other blockchains inside ENS. The draft is here; feedback appreciated.

Open questions:

  • Should setAddress and removeAddress take a coinID and iterate over the list instead of taking an index? Iteration over a short list won’t be unduly taxing, and this removes the dependence of clients on knowing the list’s ordering, which can change when items are deleted.
  • Should the backwards compatibility recommendations be made a requirement, instead? Or entirely deleted?
2 Likes

Some initial thoughts:

I’d rather see SetAddress and removeAddress not use indexes and stick with coin ID. The less implementation-specific knowledge required by the users the better.

I would also like to see an address(uint256 coinType) bytes function in here. Realistically most clients are going to know which coin they want, and dealing with a single return value is easier than handling an array.

I hate seeing SHOULD in RFCs, they always seem to end in confusion when it comes to implementation. To that end it would be great if “The list SHOULD contain at most one entry for each coin type” could be changed to “The list MUST contain at most one entry for each coin type”. I don’t see it costing much more gas in the common case, and it removes a potential source of confusion (returning an arbitrary address doesn’t seem to adhere to the principle of least surprise).

Regarding backwards compatibility, I’d be inclined to make multicoin the successor to addr, and as such multicoin lookups always treat addresses in the coin array as primary:

  • if there is information in multicoin with coin type 60 use it, ignoring information in addr
  • if there is no information in multicoin with coin type 60 use addr
1 Like

(oh yeah I think you meant 165 not 168 for the interface definition)

1 Like

What happens if ETH id is set on both setAddr and setAddress but as a different value? Which takes priority?

The following only says it appends but doesn’s talk in case of conflict.

When addresses is called, check if the addr field is set. If it is, append an item to the returned list with the coin ID for Ether (60) and the address stored in the addr field.
1 Like

What is coinID? You mean coinType? Can you handle both cases so that if index is not specified iterate over and if specified access directly? This case, people can omit index for the majority of time but be future proof incase people start adding 1000s of cointypes which may run out of gas.

I’m puzzled by the choice using an array, which has to be traversed, when a mapping could be used instead.

Why not replace

mapping(bytes32=>AddressInfo[]) _addresses;

with

mapping(bytes32=>mapping(uint=>bytes)) _addresses;

In the resolver ?

In order to query an address, you would just have to lookup the value of a specific coinType for a given node, without having to translate the cointype into an index.

And then why stop here ? Just have a generic

mapping(bytes32=>mapping(bytes32=>bytes)) _metadata;

with methods

function setMetadata(bytes32 node, bytes32 key, bytes memory value) public onlyOwner(node);
function metadata(bytes32 node, bytes32 key) public view returns(bytes memory);

Which could be used for ANY metadata (similar to what ERC725 proposes) and use specific keys to store addresses. For exemple metadata(node, keccak256("multicoin.bitcoin")), metadata(node, keccak256("multicoin.litecoin")). The result would either be empty (nothing here) or be interpreted according to the key.

This would not only solve the multicoin issue but could also be used for other projects. An ID project could just as well use metadata(node, keccak256("DID.photo")) to store the ipfs hash of a picture. A message service could also use metadata(node, keccak256("cryptography.publi.RSA")) … posssibility are endless.

Why always adding very specific fields to the resolver which call for constant redeployment when something like that could be (mostly) future proof.

Basically just like the text / setText entry but with bytes instead of string so we can store non-ascii arrays without encoding/decoding issues

An array has the benefit that it can be traversed, which is handy if I want to query which coins a domain may accept.

Although having a completely generic mapping(bytes32=>mapping(bytes32=>bytes)) everything is tempting it does tend to move the complexity rather than avoid it. There would need to be some sort of registry of key names and standards associated with each of them (would it be “multicoin.bitcoin”, “multicoin.Bitcoin”, “multicoin.BTC” etc.)

The thing is, even with the proposed implementation you already need registry of what the coinType are. Otherwize what will you do with
{ coinType: 3, addr: 0x0062e907b15cbf27d5425399ebf6f0fb50ebb88f18c29b7d93}

I personnaly think that having a cointype registry isn’t easier than having a list of hash for coins … You can check if any of there is supported directly from the blockchain, and if you have a layer on top (like graphprotocol) you can very easily list all keys for a given account in a single graphql query

Good idea.

While we can prohibit resolvers from returning lists with duplicates, we still need to tell clients how to handle it if a broken resolver does return a list with duplicates. I think saying they can pick any one they want will discourage resolvers from trying to rely on client behaviour here.

Wouldn’t it make more sense to push this to addr if it’s set? That way, addr will continue to return the same value as type 60 in addresses.

If a resolver uses my suggested approach, then setting type 60 will instead set addr, so it’s impossible for it to be in the list.

Oops, yes.

We could, but I’d rather not complicate the interface unduly.

This is why. Wallets have told us they want to be able to list all the cryptocurrencies a name has addresses for, so users can see all the balances, and it’s not possible to enumerate mappings.

We already have a text type, specified in EIP 634, but I think something as basic as address resolution warrants its own type and shouldn’t be crammed into an existing one.

This is SLIP44, as linked in the spec.

I know this field (and use it for ENSLogin), but the fact that the value is string require non ascii data to be encoded, which increases the storage size. This is particularly painfull for multiaddr that, if not encoded to an ascii compatible format, cause clients to throw errors. (I get this could be solved on the client side … maybe it should … but then what will be the point of having bytes and string as different solidity types)

I’d either not touch addr at all with multicoin, or do it the other way around i.e. addr() calls address(60) and setAddr(x) calls setAddress(60,x). The latter seems to be a better solution to keep down the burden of client code to upgrade whilst retaining a single storage slot to store the data and a simpler resolver codebase.

Hi ENS team!

We worked on Multi-crypto resolution in RNS team.

Here’s how we defined the IP: https://github.com/rnsdomains/RNSIPs/blob/master/IPs/RNSIP03.md

In the implementation we added metadata for each address: https://github.com/rnsdomains/rns-artifacts/blob/master/contracts/resolver/MultiChainResolver.sol

I think mapping to the coin type instead of looping has a better response on resolving a name. I don’t understand the importance of using indexed arrays.

1 Like

You’re right, that does simplify the codebase. It makes retrieving addresses more expensive, but this is done offchain most of the time, so it’s not a big deal.

Thanks for showing us your solution! What motivated the use of text instead of binary format for addresses? Given the cost of blockchain storage, minimising size seems important.

One thing we’ve heard from wallets is that they need to be able to get a list of supported coin types for a name. With a mapping, this is only possible if they’re indexing all the events emitted by the resolver, which is a lot more difficult for a wallet to implement.

1 Like

I think “O(1)” getting the requested address is the key on-chain. Then, other getters may be defined. Also off-chain services for indexing or restructuring all values is a suitable solution for wallet services.

Fetching addresses onchain isn’t very common - offchain is much more common. Even then, the expected number of chains per address is quite low, so iterating linearly is still tractable.

What we’re hearing from wallets is that this is not the case. They don’t want to have to rely on a third-party service like The Graph for name resolution, and they also don’t want to have to run their own custom indexing solution.

Thinking of a nice bridge…

Smells like ERC-721 balance. Not the best solution in my opinion.

I won’t use this as a standard but it looks suitable as an implementation.

Can you elaborate?

I’m not sure what you mean by this, either.

I’m working on an implementation, but temporarily stymied by a weird Truffle issue.

I believe that we should store on-chain only necessary data that representes the actual state, and provide a set of basic functions that allow as to identify each state from another.

ERC-721 provides balanceOf function, but it stores a list of balances that must be updated in each transaction to mantain the balance updated. This balanceOf could be easily implemented off-chain making the solution cheaper.

Same for indexing the resolved addresses, it tells us more than we really need to know. Just by emitting an event of an update, we can trace the actual values off-chain with no need of extra structures, looping or indexing.

This is my vision of how smart contracts can be designed to achieve the same transparency and integrity while keeping the cost lower for users.

I foresee cross-chain applications that could benefit from cheap on-chain lookup of cryptocurrency addresses for a given name.

On further discussions with wallet vendors, being able to list all the coin types isn’t as vital as previously supposed. Given that, I’ve rewritten the spec to be significantly simpler - now it adds new overloads to addr and setAddr that also take a coin type. I’ve taken @jgm’s suggestion of storing everything in the coin type map, too.

EIP draft here and PR for the public resolver here. Feedback appreciated.