Smart Contracts Aren’t Immutable!
TL;DR: Smart contracts on Ethereum are not immutable.
In this, the first collaborative article with Resonance Security, offensive security expert[1] João Simões and I look at Ethereum smart contracts, and in particular the myth that they are immutable.
To get started, there are quite a few blockchain concepts that you will need to understand —the difference between personal blockchain addresses and smart contract addresses and how the deployment of smart contract code works, so we begin with an analogy.
The bank account analogy
On Ethereum, there are two kinds of blockchain addresses:
- personal addresses, referred to as externally owned accounts (EAOs), which are like a personal bank account numbers, and
- smart contract addresses, which are like corporate bank account numbers
When you open a personal bank account, you are given a bank account number. You can then have your salary paid into the account, and use your card to make purchases in a shop or online, or conduct other financial transactions.
You don’t get to decide what your bank account number is — if you’re not happy with it, you have to open another account, and another, until you get one that you like. Fortunately, opening bank accounts on Ethereum is free, and it’s a simple task to automate. So if you’re fussy you can keep trying until you get an account number you like.
Now let’s look at the corporate bank account. Here we have to stretch the analogy, and imagine that when a corporation is set up, with corporate bye-laws and a mission statement, it follows them to the letter. For example, the purpose of the corporation may be to receive money in the corporate bank account, to use that cash to manufacture widgets, and then send them out to the people who deposited enough money (let’s say it costs $100 to get a widget). We’ll call it the First Widget Company.
As with personal bank accounts, company bank accounts are assigned what looks like a random bank account number. Except, they are not, and I’ll be explaining why, how, and what the significance of this is later on.
Corporate bank accounts are different to personal ones. As an outsider you have no idea what the next transaction from a personal account will be, because there’s a person behind it, with all their foibles and preferences. A personal bank account may be trading tokens on a decentralized exchange one day, buying non-fungible tokens on an NFT marketplace the next day, and then it may do nothing for years.
On the other hand, with a corporate bank account you know exactly how it will act. You can look at the bye-laws and the mission statement and determine that if you send 100$ to the account number of the First Widget Company, a month later, without fail, a widget will arrive in the post.
If the bye-laws do not contain a clause describing how the First Widget Company can be wound up, then you also know that for the rest of eternity you can get widgets from the company for a mere hundred bucks.
What you get is what you get
This is meant to be the main selling point of smart contracts — just as you know exactly what burger you’re going to be served if you walk into a McDonalds in Bangkok or Berlin, or what drink you’re going to receive if you buy a can of Coca-Cola, with a smart contract what you get is laid out clearly in the contract code.
Well, clearly, if you can read and understand Solidity programs, or some other smart contract language.
This immutability presents a problem for smart contract developers. What if, later on, we want to raise the price of widgets to $200? Or drop it to 50$? If the bye-laws for the widget company don’t contain a clause for altering the price, we are stuck with the original cost we decided on when the company was founded and the company bank account was set up.
We could found a new company: the Second Widget Company, with new bye-laws that allow us to change the widget price as and when we feel like it. But how do we inform faithful clients of the First Widget company that widgets are now available cheaper and quicker from the Second Widget Company?
Or worse — how to we prevent customers from using the First Widget Company if the price there is lower?
Some of you may be thinking, “Just make sure that when you set up the First Widget Company, all the possible parameters for widgets are written into the bye-laws: the color, size, weight, price, and so on.”
If only programmers were so prescient. The world is a complex place, and we have no idea what the future is going to bring, how the markets are going to change, and hence what parameters are going to be important. We may not even realize what those parameters are until months or years in the future.
The widget proxy
One popular solution is to use a proxy smart contract. This is like setting up a Widget Proxy Company, with one very simple set of bye-laws: to receive money to its account, and to forward it on to a second company account. The bye-laws contain a provision that allows the second company account number to be changed at a later date. This makes the Widget Proxy Company a very simple corporate entity.
Then we can set up the First Widget Company as before, but with one simple extra addition: it will only accept orders coming via the Widget Proxy Company account.
At a later date we can set up the Second Widget Company, and alter the account the Widget Proxy Company forwards the cash it is paid to. Namely, the Second Widget Company account.
As a result:
- No one can trade with the First Widget Company anymore,
- Widget customers don’t have to update their list of bank accounts to include the Second Widget Company account, because they keep using the Widget Proxy Company account, and
- The widget developers can release as many upgrades as they like — there can be a Third Widget Company, or a fourth, and so on, each one functioning differently from the previous one. This allows for bug fixes and other changes.
The downside of all of this is that if the developers release a Malicious Widget Company upgrade that simply takes money deposited in its account and sends it off to the Cayman Islands without ever manufacturing and sending out a single widget, and then the developers point the Widget Proxy Company to the Malicious Widget Company account, unsuspecting customers may find that their money starts to be stolen.
This is why every time you interact with a proxy, you have to check that it is still pointing to the original smart contract you were previously using, and if it doesn’t you have to audit the new contract to ensure it is not doing something nefarious.
Metamorphosis
There is another way to update a smart contract that is even more nefarious than the use of proxies, using a technique known as “metamorphic smart contracts”. It is a bit more complicated than using proxies, but we can still investigate it using the bank account analogy.
With the proxy solution, we kept the gateway or entrance contract the same, so it has the same “bank account number”, but we can change which underlying contract it points to. This change can be detected, because when the proxy is made to point at a new underlying contract, that contract has a new address. There is a clear indicator that something is different.
But back in February 2019, Ethereum introduced an upgrade called the Constantinople hard fork[2] that introduced a new feature allowing an initial company to be able to found new companies with a predetermined bank account number. And provided the new company has a clause in its articles of incorporation that allows it to be completely wound-up and dissolved, the initial company can then found a further new company, and reuse the same bank account number.
With metamorphic smart contracts there is no clear indicator that the contract has changed — we have to go to the raw data of the blockchain to see what is going on. That’s right — as of early 2019, some Ethereum smart contracts can be overwritten using a clever trick. That’s like allowing the reuse of bank account numbers or credit card numbers. You think you’re sending cash to Alice’s account, but things have changed, and now it’s Eve’s account instead.
A trick of the tail
So how does the trick work? For that, we have to leave our analogies behind and get technical.
The Constantinople hard fork introduced a new command (called an opcode in Ethereum-speak): CREATE2
.
The CREATE2
command allows smart contracts to deploy further smart contracts with predetermined addresses. You can think of it as providing our hypothetical company with the ability to launch subsidiaries, and know what the bank account numbers of those subsidiaries will be, in advance. And unlike the proxies of the previous section, the subsidiaries can act as fully independent companies.
The initial idea was that each time CREATE2
is called, a counter used in generating the next bank account number is increased, and that ensures that the subsiduaries always get their own unique new bank account number.
Then somebody noticed that if you include a selfdestruct
function in your subsiduary, which you call at a later date, that “wipes” your smart contract from the blockchain, and what’s more, you can then reuse the bank account number for a new smart contract with completely different code.
The metamorphic smart contract feature was used in May of this year to hack the governance DAO of Tornado Cash, by proposing an innocuous and legitimate looking smart contract that was approved, and subsequently metamorphosing it into a malicious smart contract that allowed the hacker to siphon off and sell nearly $3 million Tornado Cash governance tokens ($TORN).
The check for a metamorphic smart contract is therefore:
- does it contain a
selfdestruct
function? - was it deployed from another smart contract using
CREATE2
?
If both of these are true, you need to check the smart contract every single time before you interact with it. And this is not something that the average Ethereum user is going to be able to do with ease.
Conclusion
In today’s article we have looked at two methods for providing mutability in smart contracts — the capability of changing the code that makes up a smart contract or collection of smart contracts over time.
A blockchain may be immutable in its entirety, but in practice that immutability does not carry over to the smart contracts running on it.
And although blockchains provide transparency, for most people it is not an especially transparent form of transparency.
Footnotes
[1] by which I mean that he is an expert in offensive rather than defensive security, not that he is offensive and a security expert.
[2] see https://eips.ethereum.org/EIPS/eip-1013
[3] see https://cointelegraph.com/news/attacker-hijacks-tornado-cash-governance-via-malicious-proposal