Selfdestruct operation on Upgradeable contracts

Hi

I have a question about the operation selfdestruct with upgradeable contracts.

The "protection" in the documentation, here, is to insure that the call of the function can only made through the proxy ?

But if it is triggered through the proxy, the destroyed contract will be the proxy (not the implementation), is it correct ?

If it is the right behavior, I think it would be good to indicate in the documentation that the call will destroy the Proxy.

For me, if the proxy used is a transparent Proxy, destroying the proxy is a more dangerous operation than destroying the implementation contract, which can be changed.

1 Like

In the example they provided, they are trying to warn users that using selfdestruct in upgradeable contracts may have unexpected side-effects that would be non-obvious to a new solidity developer. If you were writing a standard smart contract and call selfdestruct, the default and expected behavior would be for the contract to be destroyed. There may be many reasons why you would want to do this, but let us look at a simple example:

contract Normal {
  address payable public owner;

  // Initialize the contract on deployment to set the owner's address
  constructor() {
    owner = payable(msg.sender);
  }

  // Privileged functions should only be allowed by the owner
  modifier onlyOwner() {
    require(msg.sender == owner, "Not Owner");
    _;
  }

  // Only the owner should be able to destroy the contract
  function destroy() external onlyOwner {
    selfdestruct(owner);
  }
}

In this example, the person who deploys the smart contract and ONLY the person who deploys the contract can call destroy, which would then destroy the smart contract. This would be the expected behavior. Now, let us look at this same code but instead make it a UUPS Upgradable smart contract:

contract Upgradable is UUPSUpgradeable {
  address payable public owner;
  bool private initialized;

  // Initialize the contract on deployment to set the owner's address
  function init() external {
    require(initialized == false, "Already Initialized");
    initialized = true;
    owner = payable(msg.sender);
  }

  // Privileged functions should only be allowed by the owner
  modifier onlyOwner() {
    require(msg.sender == owner, "Not Owner");
    _;
  }

  // Only the owner should be able to destroy the contract
  function destroy() external onlyOwner {
    selfdestruct(owner);
  }

  // Only the owner should be able to upgrade the contract
  function _authorizeUpgrade(address) internal override onlyOwner {}
}

If we were to deploy this as a UUPS upgradeable contract, at first glance, it would appear to do the same thing as the non-upgradable contract, with the exception that more features could be added to it at a later time by the owner. However, because UUPS contracts store the upgrade mechanism in the logic contract and not in the proxy, an attacker could call init() followed by destroy() on the logic contract, which would destroy the logic contract that the UUPS proxy points to. Because the upgrade code for UUPS proxies is in the logic contract and that contract has now been destroyed, the proxy will forever point to an empty address, and there will be no way to recover it. This behavior may be non-obvious since, typically, the state of a logic contract does not affect a proxy contract.

4 Likes