Want to add ENS name support in your smart contract? The goal of this article is to help break this down and provide some examples.
Pre-requisite reading
The ENS documentation has a high level playbook for dApps that want to enable ENS support. If you haven't read it yet, open it up, give it a read, and then come back (it's very short).
But to summarize, they outline three levels of support that you could provide for your dApp
- Resolving ENS names
- Support reverse resolution
- Let users name things
The documentation says that level 1 is the easiest to achieve and provides the highest impact to users. In other words, if you only do one of these things, it should be level 1.
I agree that this is the most high impact ENS support think you can do, but I also think that depending on your scenario, achieving this is a little more complex than the documentation is letting on. The rest of this article will focus on level 1, providing a sample implementation and exploring some of the challenges you may need to solve to achieve this in your dApp.
Also, go read this page which discusses how the ENS smart contracts store ENS names in a bytes32
field.
Next, it helps to read about how Solidity stores addresses internally. Basically, the address
type holds a 20 byte value. You can convert a bytes20
into a uint160
, which you can then convert into an address
. This will come in handy later when we want to store Ethereum addresses side by side with ENS names in an efficient data structure.
If you're not familiar with byte manipulation in Solidity, this tutorial seems like a useful primer.
Why this is harder than it seems
If possible, when a user enters an ENS name instead of an address, remember the ENS name, not the address it currently resolves to. This makes it possible for users to update their ENS names and have applications they used the name in automatically resolve to the new address, in the same way that you would expect your browser to automatically direct you to the new IP address if a site you use changes servers.
From the ENS documentation on ENS Enabling your Dapp
The documentation is very clear here about what you should do. You should "remember" the name and not the address that the name resolves to. So if your user enters an ENS name into your frontend like "alice.eth", your Dapp should remember "alice.eth" and not the 20 byte 0xblah Ethereum address that alice.eth resolved to at that time.
The advantage to this approach is that you empower the user to change the address that their name resolves to, without needing to update the state of your smart contract (browser DNS analogy).
So that all sounds great - but how do you actually do that?
The naive approach
The "easy" way to allow your smart contract to support ENS names is to ignore part of the suggestion above and not remember the ENS name, but just remember the address that the ENS resolved to at the time the user interacted with your contract.
This is pretty easy to do if your frontend is built with one of the many frontend libraries that support ENS (web3.js, ethers.js, etc...). This documentation from ENS does a great job of overviewing how this work. If you want to get an even deeper look into the nitty gritty, consult the documentation for your frontend library.
In most cases, you don't actually have to do anything to get this functionality. Chances are that if you pass an ENS name to your library, it will automatically resolve to an Ethereum address without any extra code on your part.
This is the bare minimum for supporting ENS, and the best part is you don't have to do anything. But like we previously discussed, this does not technically follow the suggestion to store the name, and not the address that the name resolves to. So how do we do this?
The more complicated, but cool approach
So what would it look like if we actually wanted to do what ENS is suggesting (store the name, and not the address)?
Well, this definitely might complicate your smart contract's data structure. For example, if you take the naive approach, you can just track an address[]
. If we want to give the user an option to store an ENS name, then we either need to complicate our data structure or use a second complementary data structure.
In other languages like Javascript, you could simply use a heterogeneous array, maybe something like (address | ensName)[]
. Solidity doesn't support this, so we need a different approach. Two ideas come to mind, at first glance:
- List of struct, where the struct contains an
address
and abytes32
field to store an ENS name. Then, your smart contract would have to fill one of those fields at write time. And at read time, it would have to decide which field to read (depending on which has a non-default value). - Two data structures. Maybe
address[]
andbytes32[]
? Your smart contract would write to one or the other, and would need to check both at read time
Neither of these feel great. They both involve wasted storage space and/or weird indexing. Both of these approaches should work, but they may make your contract logic complicated, which you typically want to avoid.
Ideally, our data structure that tracks a list of addresses should be homogenous. That means that the type of each item in our data structure should be the same, even though the items in our list will sometimes be ENS names and sometimes will be plain old Ethereum addresses.
But we just said that Solidity can't do this - so how would this event work?
Using a bytes32[]
If you recall from the pre-req, the Solidity addresses
type stores a 20 byte value.
On the other hand, the namehash value that ENS deals with is a 32 byte value (i.e. the internal value the ENS smart contracts store for ENS names like 'alice.eth').
So what we can do is store our addresses in a bytes32[]
data structure. This will allow us to store normal Ethereum addresses and ENS names in a homogenous array (all items are of the same type).
But you might be wondering what happens with the 20 byte Ethereum address values? They are too small to fit into a 32 byte value. Well, that's true, but we can easily "right pad" with zeros. In other words, we would add a null byte ("00") to the right side of our 20 byte value, 12 times. This way, we can convert our 20 byte value to be 32 byte.
If I loop through such a data structure and print out each 32 byte chunk, it could look something like this
0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec
0xba744dde23446485cb4a175d8e78eeff2063875d000000000000000000000000
0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec
You can see that some of these lines have 12 null bytes at right side - these are our padded ethereum addresses! The others are implied to be ENS nodes (i.e. namehashed ENS names)
Putting it all together
I've drafted a sample smart contract to give a more concrete example of what this could look like. You can read the entire source code in this Github repo, which is a Hardhat project that you can run, complete with unit tests.
I am calling this sample project AddressHodler
and it's deployed on Rinkeby at 0xa746B0D0E24d3863c9194cF14eAd8eD22dAF4f75, with the source code verified
If you're curious what an implementation of this strategy might look like, check out the source for AddressHodler
. It's a really simply smart contract, that quite literally hodls addresses.
Storage
You can call its addAddress(bytes32)
method to provide it an address or name hash and it will store it. Notice that the type passed in is bytes32.
Your frontend will need to namehash ENS names with eth-ens-hamehash in order to produce this bytes32 if you are trying to store an ENS name.
If you're trying to store an Ethereum address, then your frontend will need to right pad the address with 12*2 zeros to fill it out at 32 bytes.
Retrieval
In order to retrieve an address at a specific storage index, you can call its getAddress(uint _index)
method. The smart contract will automatically detect the type of the bytes32
stored in its addressess
field, and then convert it to an Ethereum address
If the bytes32
stored at that index is an ENS name, it will resolve that ENS name by calling the ENS smart contract
If the bytes32
stored at that index is a simple Ethereum address, it will simply truncate the last 12 null bytes and return you the corresponding address
type.
This is really cool - because users of your dapp who registered with their ENS name retain control of the address your smart contract resolves, and your smart contract gets to sort of interop between ENS names and addresses!
Testing our smart contract
If you want to test out the AddressHodler
contract, I've seeded it with some sample data that produces known output.
I called the addAddress
method twice
For the first call, I passed the name hash of 'alice.eth', which I got by calling namehash.hash('alice.eth')
using eth-ens-namehash
Name hash of 'alice.eth'
0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec
By the way, if you go over to app.ens.domains, and look up 'alice.eth' while your wallet is connected on Rinkeby, you should be able to confirm that this address resolves to 0xFB338C5fE584c026270e5DeD1C2e0AcA786a22fe (although technically whoever owns alice.eth could technically change the resolver at any time)
For the second call, I passed a random Ethereum address I generated. But first, I right padded it with 12 bytes.
0xba744dde23446485cb4a175d8e78eeff2063875d000000000000000000000000
Now let's see what we get when we call getAddress
for index 0 and index 1
Index 0
As expected, this resolves correctly
Index 1
And this one is also correct, it's just the Ethereum address we had previously supplied but with the null zeros truncated
Wrapping up
Voila! Now you know how to store ENS names and Ethereum addresses in a homogenous array in Solidity. It's not exactly pretty - but it's definitely doable.
This is just one possible approach. Some of the other approaches I mentioned earlier and chose not to use are totally valid, Some of this boils down to personal preferences and priorities.
Another approach that I think makes a lot of sense is to keep an array of structs, where the struct looks like
struct Data { bool IsAddress; bytes32 data; }
This would allow you to store the type of the data, which allows you to simplify the logic at retrieval time. Another advantage of this other approach is that you get to mitigate a potential vulnerability (is it possible for an ENS namehash to end in 12 null bytes? probably not, but if it is, that's a potential vulnerability that would be mitigated by this struct approach).
Lots of credit to Serenae.eth for the technical support in the ENS Discord. They had the original idea and I just implemented it to see what it would look like.
Thanks for reading!