EIP-4758: Deactivate selfdestruct

GitHub - jwasinger/eth-selfdestruct-analysis . This is an updated impact analysis which looks at usage of SELFDESTRUCT after the London hard fork and identifies contracts with large holdings that could be affected by the changes in EIP-4758

As mentioned by @chfast SELFDESTRUCT can be used to burn ETH by ‘selfdestructing’ to the current address. There is another quirk however which is changed by this EIP:

SELFDESTRUCT does not immediately destroy code and send the balance. This is done after all call frames are done and thus every opcode of the transaction has been executed. Only then are the following steps executed:

1. Send all ETH of the contract to the beneficiary
2. Destroy the contract (code = empty, balance = empty, nonce = 0, balance = 0)

Due to this order it is thus possible to destroy ETH. Also, notice that it is possible to do “multiple” selfdestructs by calling the selfdestruct from another contract multiple times. It is possible to first SELFDESTRUCT to address A and then SELFDESTRUCT to address B. In this case, the beneficiary is B, not A. If this changes, it should be specified on the EIP. It now seems that SENDALL sends all ETH of the current contract to the beneficiary and immediately exits the current call frame - which I guess is fine, but the EIP also states that it “renames” SELFDESTRUCT to SENDALL which is due to these quirks not really the case.

I don’t believe this is correct. The ETH is sent immediately. @gumb0 has double-checked this.

Oops, you are right, have semi-deleted my post.

I am a consumer of the reincarnation upgrade pattern. I built an NFT contract ownership system (0x000000000000c57CF0A1f923d44527e703F1ad70 on every chain) to facilitate this pattern. It helps us upgrade our contract without needing to re-approve and migrate every token for every protocol. Our storage footprint on the protocol would be much larger without SELFDESTRUCT. The SLOAD+DELEGATECALL pattern, especially after Berlin, costs way too much for practical use in gas-denominated auctions, so we cannot use it.

I am willing to pay millions in gas to facilitate code changes. I do not see why code must be immutable but not storage. Perhaps there is a fair way to price a code change, and we could introduce a replacement opcode like SETCODE.

The main drawback of SELFDESTRUCT right now from an engineering perspective is that it doesn’t take place until the end of the transaction, so I need two separate transactions to do the upgrade. This has prevented wider adoption, as you could not safely upgrade a token, for example, without risking being sandwiched.

5 Likes

Our production system heavily utilizes the CREATE2 and SELFDESTRUCT loop. We have a few million dollars of TVL, with more expected to arrive in the near future. The system wasn’t designed to be modified once deployed and this EIP would fully brick our value-storage system, rendering funds inaccessible for users.

As a result, I’m very opposed to this EIP. It breaks a system that allows us to deploy and undeploy smart contract proxies within the same block, which is good not only for gas costs and allowing lower storage utilization, but also for security. By never having code deployed on those contracts outside of very predetermined periods, we reduce our attack surface area significantly. It is frustrating to be punished for being at the forefront of security in smart contract development, and we strongly request that this EIP be reconsidered or modified in some way.

Perhaps a change to CREATE2 could be considered in concert with this, such that it checks for existing byte-code and quietly fails rather than reverting. That would allow our system to continue to function unimpeded by the implementation of this proposal.

10 Likes

Is there any progress on this EIP-4758??

What do you think on repricing CREATE and CREATE2 when constructor returns zero size? Nothing will be deployed, only constructor code will be executed from new address. Now this costs 32k

I have no strong opinions on this EIP and whether or not it is the right path forward.

Did want to flag for visibility though as part of this conversation, that this has impact on the BytecodeStorage.sol library that we introduced in our ERC721-conforming “CoreContract” at Art Blocks here: Contract bytecode for script storage by ryley-o · Pull Request #299 · ArtBlocks/artblocks-contracts · GitHub.

This would not be a breaking change perse, so if this EIP were approved+implemented it wouldn’t be a dramatic concern point for our team by any means.

That said, a meaningful part of our rationale for adding this functionality in the first place was the intention to be mindful custodians of our impact on state bloat (see Allow for cleanup of unused contract bytecode for script storage by jakerockland · Pull Request #304 · ArtBlocks/artblocks-contracts · GitHub for context).

It sounds like this rationale of ours may not hold water vs. the concerns around the state management complexity that comes with SELFDESTRUCT which is totally valid and again not a matter that we have a strong opinion on.

tl;dr, we’re making use of SELFDESTRUCT at Art Blocks, but it is in a way that is not dramatically impacted by this EIP in a way that we have strong concern about–I am sharing all of this for additional context/visibility and not because we have a strong opinion on the matter.

2 Likes

Not sure if this actually fully solves the problem that this EIP intends to capture, and again I definitely do not have a strong horse in this race, but has the approach of functionality limiting the amount of state change caused by a given usage of SELFDESTRUCT, by way of changing the op-code pricing for the opcode in order to more directly bound state change to the current gas limit, an approach that has been considered?

Could definitely understand that this type of approach would be impractical with regards to the client complexity it would add to calculate gas costs in a way that fits the state-change-bounding constraints that are driving this PR, but figured I would ask! :smile::purple_heart:

Related discussion regarding Shanghai inclusion: Shanghai Core EIP Consideration - #35 by wjmelements

That is an interesting idea. You could probably set it up so that CREATE2 will allow you to deploy the exact same bytecode to the same address, but fail for all other code.

I’d be concerned about the case where someone expects SELFDESTRUCT followed by a CREATE2 to clear storage, which wouldn’t happen.

Could you elaborate?

Basically, our system uses ERC-1155 to represent Financial NFTs, which are backed by tokens. Say an NFT is worth 10 tokens each, it has ID=10, and a supply of 15. 10 copies of ID=10 are owned by User1 and 5 copies of ID=10 are owned by User2

The code our system uses is as follows:

                address smartWallAdd = Clones.cloneDeterministic(TEMPLATE, keccak256(abi.encode(fnftId)));
                RevestSmartWallet wallet = RevestSmartWallet(smartWallAdd);
                amountToWithdraw = quantity * IERC20(asset).balanceOf(smartWallAdd) / supplyBefore;
                wallet.withdraw(amountToWithdraw, asset, user);

The OpenZeppelin method follows:

    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
     *
     * This function uses the create2 opcode and a `salt` to deterministically deploy
     * the clone. Using the same `implementation` and `salt` multiple time will revert, since
     * the clones cannot be deployed twice at the same address.
     */
    function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
            mstore(add(ptr, 0x14), shl(0x60, implementation))
            mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
            instance := create2(0, ptr, 0x37, salt)
        }
        require(instance != address(0), "ERC1167: create2 failed");
    }

So if User1 withdraws all of his FNFTs, then User2 will find that his are permanently inaccessible because the contract already exists, as the self_destruct that would have occurred at the end of withdraw was never triggered

1 Like

This EIP seems to be missing some details that should be considered:

  • Doesn’t specify whether SENDALL should halt execution like SELFDESTRUCT does
  • Whether the gas cost should be adjusted
  • Providing an alternative for replacing code at a given address
  • Plans for backwards compatibility beyond “get rekd”

The last I find is specifically important. As a platform for building immutable, composeable dApps breaking some functionality which some projects seem to critically rely on sets a bad precedent. There is some small precedent for mutating contracts via social consensus (DAO hack) but the circumstances were very different there.

Beyond harming users of such dApps it would encourage more immutability and upgradeability as devs may want to ensure that they can fix their protocol should the protocol choose to one day break it.

2 Likes

I agree the EIP could use a bit more clarity, but my interpretation is that the opcode continues doing what it did before except the changes listed. This means it will still halt execution just as before. The same thing for gas cost (doesn’t change except for the removal of the refund).

1 Like

Having SENDALL halt execution like SELFDESTRUCT does make sense, otherwise it could lead to quirky vulnerabilities whereby code from another branch after the <0xff> can all of a sudden be reached from the branch that originally ended in SELFDESTRUCT.

However if the <0xff> opcode is getting such a significant overhaul I’d argue for a reduction of the static gas cost. At least down to the static cost of a CALL (100) or even lower considering SENDALL would not initiate a new call context and also terminates the current context.

EDIT: In fact opcodes that end the current context (STOP, REVERT, RETURN) typically have a base cost of zero so arguably the static cost of SENDALL should also be 0.

2 Likes

If selfdestruct were changed to send-all with the exception of when a contract is created and destroyed in the same transaction, would that satisfy your needs? Does your system only have a hard dependency on this pattern intra-transaction, or do you sometimes create in one transaction and destroy in another (such that you need to be able to re-use that address)?

Does this affect the non-SELFDESTRUCT contract code or storage in any way?

It seems the contract code is perpetually accessible and functional, which kills usage of SELFDESTRUCT as a form of “deactivate” or “disable” switch? (there are of course alternative methods to implement this feature)

Since contract code is currently capped, could we allow SELFDESTRUCT to destroy (delete) code whilst storage persists? In the absence of code deletion, the contract could still be deactivated.

However it could interact quite messily with the following [from the verkle tire HackMD doc at top]:

Instead, we add a new Verkle tree that starts out empty, and only new changes to state and copies of accessed state are stored in the tree. The Patricia tree continues to exist, but is frozen.

Would 100% fix the potential issue. Hard dependency is intra-transaction, contracts will always be destroyed at the end of the same transaction they were created