Logo
Czar102's Website

About smart contract immutability

Information services often describe smart contracts as immutable programs running on blockchain technology. In reality, smart contracts have simply some well-defined behavior. But to what extent are they immutable?

I hope this article will provide valuable information for EVM developers and fellow auditors on the rules by which EVM bounds smart contract code changes.

The analysis

Full smart contract immutability would mean that the code of a smart contract (code of an address) can't change. It is trivial to find a violation of this invariant: simply deploying the smart contract to an empty address will change the corresponding code from a zero length array to (in nontrivial cases) a nonzero one.

So, if the codesize at an address is zero, the code can change (upon the contract deployment). But can the code anyhow change if the initial code is nonzero?

Yes! There is an opcode called SELFDESTRUCT which sends all native funds in the contract to a designated address and deletes all code from the address. So, it is not only possible to change the code from zero to nonzero, but also from nonzero to zero.

It is important to note that even if the code doesn't contain the SELFDESTRUCT opcode, its code may be deleted. This is because the DELEGATECALL opcode allows for the execution of another contract's code. Hence, if the contract has SELFDESTRUCT or DELEGATECALL opcodes, it may turn its code to a zero array. If it doesn't contain any of the above opcodes, it is safe to assume the code will not change (as long as there won't be any future breaking changes in the EVM).

We can change the code from zero to nonzero, from nonzero to zero. Not too immutable.

Creation and destroying doesn't have to be the full cycle of the smart contract. If we could deploy the contract again, we could in total carry out the state transition from existing code to existing code. And those codes could potentially be different! Hence, the contracts could change their code arbitrarily!

The only issue now is: how to deploy a contract to the same address? Every address has something called a nonce. It is used in EOAs (Externally Owned Accounts, such having respective private keys, not a smart contract) to make a signed transaction executable only once, as the transaction needs to have the current address' nonce and it's incremented when used. When an EOA deploys a smart contract, the address of the smart contract created is a trimmed hash of the deployer address and transaction nonce. Hence, if there is no partial hash collison (which is cryptographically safe to assume), it is impossible to deploy a contract to the same address. Smart contracts using the CREATE opcode also increment their nonce, and only then. Hence, that does not help.

There is another way to create smart contracts though. One can use CREATE2 which allows for deterministic deployment addresses – it takes into account the deployer address, salt and the initcode hash! Hence, we can deploy to the same address even though we already deployed to it earlier, provided that there is no contract at that address anymore. But, one may think: because the initcode hash needs to stay the same, initcode needs to be the same and hence deployed code needs to be the same. But that's not the case – initcode can construct a contract by taking the code from somewhere else than itself, for example from another contract. It can also execute another contract's code using DELEGATECALL, which removes constraints on the constructor of the contract. Hence, the new, reinitialized contract can have an arbitrary constructor and final code.

This type of contracts, those that can be destroyed and then redeployed with any code using CREATE2 are called by the Ethereum community metamorphic contracts.

The rules

An address may (but also may be unable to, depending on the code itself) change its code:

An address can't change its code:

Technicalities

The SELFDESTRUCT opcode actually schedules the deletion of an account instead of deleting only the code from the state instantly. This causes some interesting behaviors of EVM.

For example, if SELFDESTRUCT was invoked and the state was left corrupt, a call to that contract in the same transaction would execute normally, which could lead to some undefined (potentially dangerous) behavior.

According to eip-1014 which has proposed the CREATE2 opcode, it is not possible to destroy and redeploy the smart contract in a single transaction. This is a natural consequence of the fact that a contract cannot be deployed to an address where some code already exists.

It should also be noted that SELFDESTRUCT (0xff) has no immediate effect on nonce or code, thus a contract cannot be destroyed and recreated within one transaction.

Another surprising consequence is that it's possible to burn native Ether (decrease the total supply!) using SELFDESTRUCT. Because the address is deleted from state at the end of the transaction execution, if it contains any native Ether, it will be forever removed from the EVM environment. Not even redeployment can make it come back. This can happen if the selfdestruct argument will be the contract address itself, or if any other address sends native funds to that contract in the same transaction after invoking SELFDESTRUCT.

Deprecation

The SELFDESTRUCT opcode is to be deprecated within the EVM as per eip-6049 and a proposal of how to do it is described in eip-6780. It is proposed for SELFDESTRUCT to send all native funds to the argument if the executing contract has existed at the beggining of the transaction, else (if the contract has just been created in this transaction) it works as it currently is – schedules account deletion for the end of the transaction.

Hence, developers and auditors, do not use functionalities that depend on SELFDESTRUCT. Point out deprecation to current users.