Let’s Talk ENS Migration (Post-Mortem)
A deeper dive into CVE-2020–5232 and how we handled it.
As you probably know by now, the ENS registry was recently migrated from the old LLL contract to a newer Solidity contract. The reason was a bug we have not yet made public found by the one and only samczsun. This post will give more details of how the ENS team handled the situation.
On November 8th, the night before Nick Johnson’s wedding, we were forwarded an email from Martin Holst Swende that was submitted to the Ethereum bug bounty program.
This writeup references the ENS registry: https://github.com/ensdomains/ens/blob/master/contracts/ENS.lll
The owner of a node is located in storage[node + 0x20], but can write to storage[node] and storage[node + 0x40] to set the resolver and TTL, respectively.
If I set my own address as the resolver for $node, then the contract thinks I own `(node — 0x20)`. If I now sell $node to someone else, I can take back control at any time by setting the TTL of `(node — 0x20)`, which will write to `(node — 0x20 + 0x40)` = `(node + 0x20)`. If I wanted to try and mask this, I could jump backwards/forwards by a handful of fake nodes, making it more expensive to clear out all of my backdoors
Also, storage is implemented in terms of slots of 32 bytes, so it seems weird that this code is jumping around in increments of 20 slots when it can just use the adjacent slots.
For those less technical, what this essentially boils down to is: if I set up my ENS name in a certain way, and transfer the ownership to someone else, I could later get the ownership back. This would be pretty bad, so we realized relatively quickly that we had to migrate our entire infrastructure to a new registry — something that is easier said than done, considering that the ENS registry was the one component designed not to be migrated.
We immediately started investigating the scope. At the time of investigation there were roughly 310,000 registered .ETH names. Each of which would require updating. Additionally there were roughly 50,000 subdomains, .XYZ names, .addr.reverse, etc, which would all also require updating. There were at least 60,000 names which use a resolver, each of which would need to be updated. Finally there were at least 37,000 names with addresses set, all of which would need updating.
Accounting for what needs to get updated, we’d be modifying roughly 847k storage slots. Which would result in 16,940 Mgas, or at 1 Gwei and $180/ETH, around 17 ETH or $3,049 in migration costs, before overhead. In the end however it cost us roughly 140 ETH or a total migration cost of $25,200 at $180/ETH.
So no easy operation. Let’s dive a bit deeper into the scope.
Names who are registered by externally owned accounts could be migrated automatically. However we identified roughly 874 names owned by 241 distinct accounts which could be problematic. These were:
- Those that have had their registrant set to an inaccessible address (eg, 0xdead or 0x1)
- Those whose registrant is a contract
Of these most names were owned by a cold storage address, or accounts that have never sent a transaction but seem completely valid. 198 of the names however were owned by a contract, we were able to verify that with the exception of the subdomain registrar all were some form of multisig or DAO contract with an upgrade or transfer mechanism, or the ability to send arbitrary transactions. However, 16 of the names were set to an inaccessible address (0xdead, 0x1, or the base registrar address). Each of these either had the controller set to an externally owned account (and thus able to handle a migration) or to an inaccessible address.
Thanks to the help of Martin Holst Swende, we compiled a list of all the smart contracts which may need upgrading. These were contracts that either have the ENS address stored in the contract code or in their storage. The list contained ~6k contracts.
Additionally, we identified a large amount of repositories that reference the ENS contracts somewhere in their code. We would need to write a bot later to automatically upgrade these.
Upon investigating the vulnerability further, we were able to say with a large certainty that the vulnerability was not exploited. Therefore we were able to give ourselves a reasonable timeline to work with.
On the 27th of January we would deploy a new ENS contract. The new ENS contract is written in solidity and contains a fallback to the previous ENS registry. Additionally we would already inform trusted teams of the vulnerability with an actual technical report so they could recognize the severity.
January 29th was when we would announce the vulnerability publicly and lay out the migration plan. We deployed the new ENS contracts, informed the ENS multisig key holders, and published technical guides on our documentation as well as articles on our public channels. Additionally we registered a CVE number and pushed those automatically using Github’s security advisors. This would automatically inform anyone using our code. We would also run a bot to create pull requests updating ENS addresses in Github repositories.
On the 3rd of February the migration process would be initiated and the old registry would be deactivated. This required the ENS key holders to sign multiple transactions for the upgrade process, Nick then also ran and monitored a script over several hours to migrate all the names we could automatically. The migration process would end on the 10th of February.
Hopefully this post provided you with a bit of insight on what was required to upgrade ENS. You can view the fully detailed report which we wrote internally here.