Why Selfdestruct Is Not Used in Upgradeable Smart Contracts
Introduction
In Solidity, the selfdestruct
opcode is used to permanently remove a contract's bytecode from the blockchain and transfer its Ether balance to a designated address. In non-upgradeable (traditional) contracts, this functionality can be useful for terminating a contract under certain conditions. However, in upgradeable smart contract architectures—where a proxy holds the state and delegates calls to a separate implementation contract—using selfdestruct
is highly discouraged. In this article, we explore the reasons behind avoiding selfdestruct in upgradeable contracts, along with code examples.
Understanding Upgradeable Contracts
Upgradeable smart contracts separate the state from the logic:
Proxy Contract:
Holds the state (storage) and forwards calls usingdelegatecall
to the implementation (logic) contract.Implementation Contract:
Contains the actual business logic. The proxy’s functions execute using the implementation's code but within the proxy’s storage context.
This design allows developers to upgrade the contract logic without losing state. However, it also means that the lifecycle of the proxy and the implementation contract must be managed carefully.
The Risks of Using Selfdestruct
Using selfdestruct
in an upgradeable architecture poses two major risks:
Destroying the Implementation Contract:
If a self-destruct function is present and invoked in the implementation contract, it will permanently remove the contract's code from the blockchain. Since the proxy relies on this code viadelegatecall
, the proxy will then have no logic to execute, effectively "bricking" the contract system.Destroying the Proxy Contract:
Alternatively, if self-destruct is implemented in the proxy and gets invoked, the entire proxy (which holds the state) will be destroyed. This leads to irreversible loss of all stored data and the ability to upgrade, defeating the purpose of having an upgradeable design.
Code Samples
Example 1: Selfdestruct in a Standard (Non-upgradeable) Contract
In a typical, non-upgradeable contract, self-destruct can be used safely (with proper access control):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StandardContract {
address public owner;
constructor() {
owner = msg.sender;
}
// Only the owner can destroy this contract.
function destroyContract() public {
require(msg.sender == owner, "Not authorized");
selfdestruct(payable(owner));
}
}
In this example, calling destroyContract
removes the contract’s bytecode and sends any remaining Ether to the owner. This pattern is acceptable when the contract is deployed directly.
Example 2: Selfdestruct in an Upgradeable Contract (Unsafe Practice)
Consider an upgradeable implementation contract that uses an initializer instead of a constructor:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract UpgradeableImplementation is Initializable {
address public owner;
uint public data;
// Initializer function replaces the constructor.
function initialize(uint _data) public initializer {
data = _data;
owner = msg.sender;
}
function updateData(uint _data) public {
require(msg.sender == owner, "Not authorized");
data = _data;
}
// This selfdestruct function is dangerous in an upgradeable context.
function destroy() public {
require(msg.sender == owner, "Not authorized");
// Destroys the implementation contract.
selfdestruct(payable(owner));
}
}
In an upgradeable system:
If the implementation contract’s
destroy
function is called:
The self-destruct would remove the logic code that the proxy delegates to. Although the proxy's storage remains intact, the proxy would be unable to forward calls properly, effectively disabling the contract functionality.If self-destruct were somehow called on the proxy itself:
It would eliminate the state-holding proxy contract entirely, leading to permanent loss of data and control over the upgradeable system.
Conclusion
Selfdestruct is a powerful opcode in Solidity, but its use is incompatible with the proxy-based upgradeable contract pattern. Since upgradeable systems rely on the persistence of both the proxy’s state and the implementation’s logic, introducing self-destruct can lead to catastrophic failures—either by removing the implementation code or by destroying the proxy that stores the contract state. For these reasons, best practices in upgradeable smart contract design strictly avoid the use of self-destruct.
References: