SELFDESTRUCT
This post describes some reasons why the SELFDESTRUCT
opcode brings more harm than good to the Ethereum ecosystem, and so should be neutered or removed in some way. To deal with the existing contracts that use SELFDESTRUCT
, I propose some ways to eliminate the harmful aspects of SELFDESTRUCT
with minimal disruption.
SELFDESTRUCT
is not necessarySELFDESTRUCT
(originally called SUICIDE
) was introduced very early on in Ethereum's history; in fact, it was present even in this pre-announcement "spec" of the Ethereum protocol from December 2013. At the time, there was little rigorous thought being done about long-term state size management. However, there was (in my [Vitalik's] head) a general impression that to prevent state from filling up with vestigial garbage without limit, we need it to be possible for any object that can be created to be destroyed. Externally-owned accounts (EOAs), the thinking goes, would automatically be destroyed when their balance drops to zero, and contracts could have a self-destruct clause in the code to delete themselves when they are no longer needed. A gas refund would encourage them to do this.
In January 2014, Andrew Miller pointed out something that in retrospect was face-palmingly obvious: in the Dec 2013 spec, EOAs were vulnerable to replay attacks. If I had 100 coins and I sent you 10, you could simply republish this transaction on-chain ten times to clear my entire balance. This was quickly fixed with nonces. However, the addition of nonces removed all hope of EOAs being deleted: the nonce could not be reset to zero.
In 2015, some schemes were proposed to get around this and potentially allow accounts that send away all of their ETH to be safely deleted. However, by then it was clear that almost no contract developers were actually using the self-destruct feature: figuring out when to self-destruct is too hard and the rewards are too little.
By 2019-21, it has become clear that we need some other form of state management, whether rent or "expiring" long-untouched parts of the tree (aka "partial statelessness"). But if we have such a scheme, and it works, then we no longer need to care about giving contracts the ability to delete themselves voluntarily.
SELFDESTRUCT
is the only opcode that breaks important invariantsIn addition to not being very useful, the SELFDESTRUCT
opcode is not harmless. It breaks some important invariants that would be nice to have, that we cannot have because of this one single opcode.
SELFDESTRUCT
is the only opcode which causes an unbounded number of state objects to be altered in a single blockAll other opcodes work on individual values in accounts, or individual keys in storage trees, and so there is a bound on how many fixed-size objects can be changed (usually, one object per opcode call). SELFDESTRUCT, however, removes an entire storage tree.
In the present tree construction, this is bearable. However, it requires caches to be designed in a particular way with additional complexity to deal with the possibility of a SELFDESTRUCT deleting many storage slots and then a contract being created at the same address and reading those same storage slots in a subsequent transaction. Furthermore, it makes it difficult to move to a different state storage format in the future.
Two examples that SELFDESTRUCT prevents are:
Note that this is not purely theoretical; discussions about radical changes to state storage (binary trees, Verkle trees…) are happening right now, and if state storage can approximate a single key/value store with a low bound on how many keys can be changed in a block this would significantly expand the options we have to choose from.
SELFDESTRUCT
is the only opcode which can cause the code of a contract to changeThere is value in having the invariant that once a piece of code exists at a particular address, that code is guaranteed to be at that address forever. It makes it easier to build applications that depend on calling contracts that have already been deployed.
Account abstraction greatly relies on this invariant if we want abstracted accounts to be able to call libraries. But other applications as well have their security properties significantly complicated by the existence of the possibility of code changing: the Parity multisig wallet was bought down in 2017 by the library contract code being accidentally deleted.
The only opcode that breaks the code immutability invariant (and indeed, was responsible for the demise of the Parity multisig) is… SELFDESTRUCT
.
SELFDESTRUCT
is the only opcode which can change other accounts' balances without their consentSELFDESTRUCT
has a built-in "send", and this send does not execute contract code and so bypasses anti-receiving guards and logging. This risks breaking smart contract wallets, breaks other potentially useful tricks, and generally is yet another edge case that contract developers and auditors need to think about.
SELFDESTRUCT
There are two significant types of applications today that use SELFDESTRUCT
:
SELFDESTRUCT
and claiming the refund (~60% of the creation cost for near-zero-size contracts).(1) can be safely broken. The GasToken devs themselves warn that "it is extremely likely that the developers of GasToken will advocate for changes to the network that render GasToken unusable, irredeemable, non-fungible, and/or worthless". The only thing that will happen from selfdestruct refunds being removed is that some operations will become up to 2x more expensive.
In the long term, (2) is unneeded; there are other widely available patterns to allow dynamic code changing. The easiest to implement is the DELEGATECALL
forwarder, where the contract immediately performs a DELEGATECALL
-to-self using a code address taken from a storage slot; changing the storage slot will upgrade the code. In the short term, however, a few applications that use (2) exist.
SELFDESTRUCT
We choose some block FLAG_BLOCK
(eg. one natural option is the same block as the merge), at which SELFDESTRUCT
would stop working entirely. If, on or after this block, the EVM execution encounters the opcode 0xff
, it simply exits with an exception, in much the same way that it would if the EVM execution had encountered a byte that is not any valid opcode.
To give users added warning, one gimmick we can add is an asymptotically increasing gas price: if block.number + 10**6 >= FLAG_BLOCK
, the gas cost of SELFDESTRUCT
is increased to 10**10 // (FLAG_BLOCK - block.number)
.
SELFDESTRUCT
We can also change the behavior of the opcode to preserve functionality but eliminate the fact of trees getting destroyed, and add feature by which contracts can flag themselves as being un-self-destructible and thus guaranteed to not change code.
Proposed temporary new SELFDESTRUCT
behavior:
SELFDESTRUCT
, it does not get deleted. Instead, its code gets zeroed out and its nonce increased by 2**40
. A refund is not provided.SSTORE
and SLOAD
calls in a contract with address A
use the storage tree of account A_offset = (A + A.nonce // 2**40) % 2**160
Note that from an EIP-2929 perspective, A_offset
would need to be "accessed" and an additional 2600 gas would need to be charged if that account is not yet in the already-accessed set.
Another alternative is to adjust the hash function which converts storage keys into tree keys, using sha3(storage_key + contract_nonce // 2**40)
instead of just sha3(storage_key)
. Note that a somewhat similar rework would need to be done anyway to facilitate contract-level expanding-key-space statelessness.
Contracts could specify 0xA8
as the first byte in their code; the EVM would recognize this as a no-op, but use it to turn on a flag that fully disables the functionality of SELFDESTRUCT
(note: this is equivalent to the SET_INDESTRUCTIBLE proposal).
The two solutions can also be combined: immediate neutering plus longer-term full removal. Alternatively, the opcode could never be fully removed; instead, it could eventually be renamed CLEAR
and have only one piece of functionality, equivalent to a call to the target sending an amount of ETH equal to the contract's current balance.