You have probably heard that blockchains are immutable (at least under reasonably realistic security assumptions). Imagine the following magic trick:
I send you the address of a smart contract. You check it out on Etherscan and nothing is amiss.
You flip to the source code tab and see it is marked verified, and you skim the code.
I then ask you dim your screen to black and repeat a magic spell for about a minute and then hit ⌘R to refresh the site.
When you turn the brightness back up, you are still looking at Etherscan at the same address but the code is now completely different! Ta-da!
Before explaining the trick, let’s think about immutability for a minute. It is a feature: if you check out the code of a decentralized application (DApp), you know it cannot be changed tomorrow. This is also a bug: if the DApp has an error or flaw, it can never be fixed.
As a result, developers torture Ethereum into letting them change contracts. One way of doing it is telling everyone your app is running at a certain address, but all the contract at that address does is forward requests to your actual contract, which is running at a different address. If there is an issue, a new “actual” contract is pushed to Ethereum, and the “proxy” contract is updated to forward to the new contract instead of the old one. There are a bunch of issues with this because contracts hold more than code, they also hold data (for example, user balances) and that data needs to be copied to the new contract without creating race conditions. These issues have fixes but I’ll skip those details for now.
Today I want to instead tell you about a way crazier method for upgrading contracts. Imagine you are using a DApp at address XYZ. There is no forwarding or indirection, the code is running at address XYZ. Tomorrow you go back to XYZ and the code has completely changed! This should be impossible in Ethereum! And yet, it is possible, due to a strange, untended interaction between two Ethereum operations: CREATE2 and SELFDESTRUCT.
When you have coded up a smart contract and want to deploy it to Ethereum, you use a command called CREATE. You send a copy of your code from your Ethereum address and Ethereum will give it an address. Usually you don't care what address your contract is given but if you wanted to predict it, you could. It is just that every time you do any transaction, the prediction changes. In 2019, Ethereum added a variant called CREATE2. You cannot pick the address where your contract will end up but you can predict it. If you predict it and do a bunch of unrelated transactions first, the contract will still end up going to the same place.
Under the hood, the address is computed as the output of a hash of a few things. One is the contract’s “initialization code.” InitCode is subtly different from the contract’s actual code. It is more like “instructions for getting the code” than the code itself. That said, a standard InitCode will basically say: here is the contract code, copy it into the right memory location.
Now say you are creating a lot of contracts that start off as copies of the same code. It might make economical sense to copy the contract code onto the blockchain once, and then using CREATE2, the InitCode can be really short and basically say “fetch the code from this location on-chain.” The problem with this is that InitCode will be exactly the same for all the contracts, so Ethereum will keep trying to put it at the same address (which will fail if there is already a contract there). No worries though, you also can throw an arbitrary value (nonce) into CREATE2 to send each copy to a unique address.
Now where does this all go wrong (or right, if you want upgradeable contracts)?
Let’s revisit a few stems of what we have already said. Recall: “…Ethereum will keep trying to put it at the same address (which will fail if there is already a contract there).” What if you deploy a contract with the ability to SELFDESTRUCT? If the contract has been destructed, there is no longer a contract at that address, so what happens? The answer is that Ethereum will happily deploy a new contact at the same address. However this will be the exact same code that was already there.
Recall: “the InitCode can be really short and basically say fetch the code from this location on-chain.” What if you change the code that is sitting at that location on-chain? The InitCode itself is the same. Therefore the address will be the same but the code that is put at that address will be different.
Introducing the magic trick “metamorphosis:”
Take some contract code (with SELFDESTRUCT) and push it on-chain at location XYZ.
Use CREATE2 to deploy a copy of this contract by setting the InitCode to “go to XYZ and copy what you find there into the code of this contract.” When hashed, this will produce address ABC.
Wait a while for users to use your contract at ABC.
Wake up one day and change the code at location XYZ to a new malicious contract.
Run SELFDESTRUCT on the contract at ABC.
Use CREATE2 to deploy a contract with the same InitCode: “go to XYZ and copy what you find into the code of this contract.” As the InitCode itself is the same, when hashed, it will produce address ABC.
As there is no longer a contract at address ABC, Ethereum will push the new malicious contract to ABC.
This sounds great for upgradeability and scary for security. The truth is that is not so great for upgradability and not so bad for security.
Some drawbacks for upgradability:
SELFDESTRUCT destroys all the data, so you cannot change just the code, you have to blank all the data at the same time
CREATE2 can only be run by contracts that want to create other contracts, rather than users, so you need to run all your contract creation from a “factory” contract
Some comforts for security:
If you see a contract without SELFDESTRUCT anywhere, it cannot undergo metamorphosis
If you see a contract that was created from a user account, it cannot undergo metamorphosis
If you see a contract that includes its own code in its InitCode, it cannot undergo metamophosis
At least one of these conditions accounts for most contracts on Ethereum that you will actually want to use.
Monte Hall Revisited
Steven Pinker's book Rationality is a solid synthesis of things you might have read in scientific non-fiction over the years. I am definitely not the target audience for Pinker's book. I think nearly every topic he covers was already detailed in one or more of the books I've read over the past 20 years. Given all this, Rationality is still an enjoyable read! I think it is nearly a complete stand-in for 20+ other books, as many books have only a few key ideas.
I did learn a few new things as well. One topic that comes up a lot in this literature is the Monty Hall problem. From Wikipedia, it is described as:
Suppose you're on a game show, and you're given the choice of three doors: Behind one door is a car; behind the others, goats. You pick a door, say No. 1, and the host, who knows what's behind the doors, opens another door, say No. 3, which has a goat. He then says to you, "Do you want to pick door No. 2?" Is it to your advantage to switch your choice?
The paradox is that switching does help. The probability of winning goes from 1/3 to 2/3. To start, Pinker builds intuition for cracking this paradox in a common way: think about 100 doors where 98 are opened.
However, almost in passing and perhaps unintentionally, he suggests another way of thinking about the problem that I have never encountered before! The trick is to stop thinking about the choices from the contestant's perspective, which leads to confusing probabilities. Instead think through the problem from the host's perspective.
The host also has decisions to make. What are they?
The contestant picks a door.
You (the host) now have to pick another door to open. Do you have a choice or not in which door you choose to open? It depends...
Contestant guessed wrong with the first door (2/3 probability):
You (the host) have two doors to open
But you have no choice, you have to open the only door without the prize!
The contestant will always win if they switch
Contestant guessed right with first door (1/3 probability):
You (the host) have two doors to open
Since both are wrong, you can choose randomly which one to open
The contestant will always lose if they switch
Should the contestant switch? 2/3 of the time they will win if they switch while 1/3 of the time they will lose if they switch. Therefore they should switch.