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:
to a nonzero value if the code has always been zero, by any type of contract deployment
from a nonzero value to a zero value if it contains any of the
SELFDESTRUCT
orDELEGATECALL
opcodesin total, from a nonzero to nonzero code if it contains
SELFDESTRUCT
orDELEGATECALL
opcodes and was deployed withCREATE2
An address can't change its code:
if the contract destroyed itself and was deployed using a simple deployment (by an EOA or with the
CREATE
opcode)if the code is nonzero and the contract doesn't have any of the
SELFDESTRUCT
orDELEGATECALL
opcodes
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 onnonce
orcode
, 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.