Contract Upgrade Wizardry: Rooted
RicMoo — April 2, 2020
just another sprout in the wall...

The power of create2 is absurd and awesome, but it is still fairly difficult to use for even some fairly basic purposes.

One useful feature is the ability to deploy a contract which can have its code arbitrarily updated in the future, to deal with bugs or incorporate new functionality, such as new EIPs.

It should be noted that this can greatly impact the trust-profile of an Ethereum Smart Contract, so it may not be useful in many cases and upgrade paths should generally be well protected. For example, by providing a time-lock on upgrades, so users have the option (and adequate time) to leave if they don't approve of the upgrade.

Why Upgrade?

Once an Ethereum contract has been deployed, traditionally there has been no way to change its code. This adds a certain level and a specific kind of trust, which while useful, means it is difficult to resolve certain problems.

The first reason to enable contract upgrades is to correct bugs. A bug can compromise the security of a contract, render it unusable or impact the user (or developer) experience.

Another reason to allow upgrading is to add new features and compatibility. Early ERC-20 tokens did not implement newer features like ERC-223 or meta-transactions, because they did not exist at the time. If the contract was upgradable, once these new standards came out, they could be added.

A lot of contracts operate by using a proxy contract which points to another contract, allowing the creator to upgrade the contract by deploying the new contract and changing the proxy's pointer. In this case, the trust-profile is very similar to using Rooted. Forwarding every call also incurs an additional gas cost for every transaction to the contract for its lifetime.

The other option when an upgrade is necessary, is to deploy an updated contract to a new address, and migrate all users and data, which can be quite expensive. Since the address changed, there is also the additional logistic burden of tracking down all the places the address is used and getting everyone to update their NPM libraries, web front-ends, mobile apps, internet links and so on.

Now Presenting: Rooted.eth

When deploying a Contract, a deployment transaction is used, which has the data field set to the initcode from Solidity and an empty to field. The empty to field signals to Ethereum this is a deployment transaction (with initcode in the data field) and not a normal transaction.

To deploy to a Rooted Address, simply set the to address in the deployment transaction to v0.rooted.eth. The contract living at v0.rooted.ethunderstands how to evaluate the initcode and hijacks enough of the inner-workings of the EVM to deploy the contract using create2 so that no additional changes to your contract source code are required.

The constructor will run as expected and things like msg.value and msg.sender will be what you would expect in a normal deployment.

To replace your contract, you must have a way for it to call selfdestruct. Once the contract has been self-destructed, simply deploy the new contract from the same account, using the same technique, including the to field v0.rooted.eth and the new contract and its bytecode will have the exact same address as the old one.

Source Code and Command-Line Interface, Please?

The source code is available on GitHub, and a command-line interface is available as an npm package (use npm install -g @ricmoo/rooted).

To use the CLI, just give it the name of your Solidity contract and an account. Any contract deployed with that account will get the same address.

/home/ricmoo> rooted contract.sol --account wallet.json
Deploy: contact.sol
Contract Address: 0x266bBB07e802890024eBd03512FbED1E3c961d83

And there are a few extra options if you have multiple contracts in the same file, or need to pass arguments into the constructor, disable optimization and whatnot.

Usage:
rooted FILENAME [ OPTIONS ]
OPTIONS
--contract NAME specify the contract to deploy
--args JSON_ARRAY specify JSON encoded constructor args
--no-optimize do not run the optimizer
--version VERSION the version to use (default: v0)

The --version is for future use. Since the version of Lurch gets baked into Rooted, as new opcodes are added through future EIPs and hardforks, updated versions will need to be introduced. Using a different version will result in a different address, so to keep your Rooted address, you must continue using the same version. This will limit the opcodes you use in your constructor to the opcodes supported in the version of Rooted you use. Most people will not need to worry about this.

What can I use it for?

Personally, I am currently using this for a Parking Resolver, which I point all my unused ENS names to.

Any of those names will resolve my Ethereum address, e-mail address and website. If a new interesting resolver feature comes along, I can update the code and redeploy it its Rooted Address.

Then I don't need to update any of the resolver entries for any ENS name in my collection, since the resolver address didn't change, and everything just continues to work.

How does it work?

Rooted is a weird combination of two of our previous Hackathon projects, Wisps and Lurch.

Wisps use create2 to install different bytecode at the same address and Lurch allows executing EVM bytecode, optionally hijacking specific operations.

When executing the Wisp Springboard, the Wisp Bootstrap uses a custom version of Lurch, which hijacks the codecopy, the caller and a handful of opcodes to make the executing initcode think things are how you want them (but not how they actually are).

The salt used for the call to create2 is the caller (i.e. msg.sender), so each caller will always get its same Rooted address, which is different from anyone else's Rooted Address.

What is Lurch?

This is a much larger article that needs to be written, but in a nutshell, Lurchwas my ETHLondonUK hackathon project which is an EVM implementation written in EVM assembly. It basically allows an Ethereum Contract to be executed on Ethereum.

While more expensive, one of the main benefits is the ability to intercept operations and do "something else".

In the case of Rooted, that includes lying about the msg.sender and remapping where the initcode is read from (i.e. the calldata instead of code).

Other possibilities include blocking all state changes, logging an event on any storage write, adding or removing precompiles, faking or using an external contract to extend the range of blockhash(number) and any other idea that can be expressed to a Turing Machine.

During ETHLondonUK, after explaining Lurch, a common question was "what? but... why? how is it possibly useful?", which is a totally fair response.

Rooted is the first use-case to try demonstrating its potential, which to be honest, I'm still not totally convinced it's useful outside of being weird and possibly interesting. *smirk emoji*

Caveats

  • The contract's storage is destroyed when a contract self-destructs; I'll have a follow-up article soon on how to selectively mitigate this (and there are examples on the GitHub)
  • The Lurch VM requires a few stack slots to operate, so if you have a particularly stack-hungry contract, deployment may fail
  • The Lurch VM is much more expensive; a custom solution using initializers and such would be cheaper but would not work out-of-the-box
  • This should be considered highly experimental; it contains many components which were hacked together for fun and to kill self-isolation time
  • For some reason Etherscan's contract verification tool is attempting to be too smart when verifying contracts deployed by contracts; I'll bug them to provide an option to just verify the bytecode

That's all for now, folks.

Thanks for reading. I'm working on a few other articles to expand on the explanation of Lurch and how the EVM works in general, but it always takes longer than expected to write these natural-language-based entries.

But to keep up to date on these and whatever new random idea wanders across my mind, follow me on Twitter and GitHub.

Stay safe and hack the planet!

• • •

"The owls are not what they seem."