Self Destruct in Solidity: A Guide

Posted on

First of all, this function is going to be deprecated in the newer versions of Solidity. With that said, let’s talk about this powerful feature, which can be used to destroy a smart contract, freeing up storage on the blockchain. While it can be a useful tool, it can also be dangerous if not used correctly. In this post, we’ll take a closer look at self destruct and explore how to use it safely.

What is Self Destruct?

Self destruct is a built-in function in Solidity that allows you to effectively remove a contract from the blockchain and send its remaining ether to a designated recipient. Therefore, when a contract is destroyed, storage space is freed up in the blockchain as its code and data are removed.

The self destruct function is called using the address of the ether recipient as an argument, like this:

selfdestruct(recipientAddress);

The recipient address will receive all funds held by the contract at the moment of destruction. However, keep in mind that the caller will still have to pay for the gas used to invoke the contract’s self destruct call.

What happens after Self Destruct is called?

After a contract is self destructed, all references to it will now point to a bytecode of 0x, just as if it was a regular account. However, it is important to note that since the blockchain is immutable, all past transactions and contract calls will still be kept in the history of previous blocks, and cannot be removed even if the contract is destroyed. So in actuality, the contract code is in a way still kept in previous blocks.

Also, keep in mind that:

  • no assets other than ether (such as tokens) will be sent to the recipient address at the moment of destruction, so these will be lost.
  • any funds and assets sent to an address of a destroyed contract will be lost.

How to Use Self Destruct Safely

While self destruct can be a useful tool, it can also be dangerous if not used correctly. Here are some best practices to keep in mind when using self destruct:

  1. Only use self destruct when you’re absolutely sure you no longer need the contract. After a contract is destroyed, there is no way to retrieve it.
  2. Make sure you set the recipient address correctly. If you send the remaining ether to the wrong address, you won’t be able to get it back.
  3. Consider adding a logical delay before self destruct is called—for example, only allowing it to be called after a certain block number—and have it set as a permissioned action. This can give you a better opportunity of recovering important assets or data from the contract before it’s destroyed.

Example Usage of Self Destruct

Here’s an example of how self destruct can be used to destroy a contract and send its remaining ether to a designated recipient:

contract SelfDestructExample {
     address payable owner;
     
     constructor() {
        owner = payable(msg.sender);
     }

     receive() external payable {} // added for the contract to directly receive funds

     function close() public {
         require(msg.sender == owner, "Only the contract owner can call this function");
         selfdestruct(owner);
     }
   } 

In this example, the owner of the contract can call the close function to destroy the contract and send its remaining ether to themselves.

See also  How to become a Solidity developer

A vulnerability using Self Destruct

A malicious contract can use selfdestruct to force sending Ether to any other contract.

A contract without a receive or fallback function can only receive ether through a coinbase transaction (miner block reward) or if someone sets it as the destination of a selfdestruct. These are the only ether transfers a contract cannot react to nor reject.

Take this game contract as an example. The game works by receiving 1 ether at a time. If the caller of a deposit causes the contract to reach 5 ether, the contract declares them as the winner and they can claim all sent funds as the prize.

contract MyGame {
   uint public targetAmount = 5 ether;
   address public winner;
   bool public prizeClaimed;
   
   function deposit() public payable {
      require(msg.value == 1 ether, "You can only send 1 Ether");
      
      uint balance = address(this).balance;
      require(balance <= targetAmount, "Game is over");
      
      if (balance == targetAmount) {
          winner = msg.sender;
      }
   }

   function claimPrize() public {
      require(msg.sender == winner, "You are not the winner");
      require(!prizeClaimed, "Prize already claimed");
      prizeClaimed = true;

      (bool sent, ) = msg.sender.call{value: address(this).balance}("");
      require(sent, "Failed to send Ether");
   }
}

contract MyAttack {
   MyGame myGame;

   constructor(MyGame _myGame) {
      myGame = MyGame(_myGame);
   }

   function attack() public payable {
      // An attacker can call MyAttack.attack, self destructing the attacking contract while holding a balance of 5 ether or more, thus reaching targetAmount and breaking the game, making it impossible for any account to win the game
      address payable addr = payable(address(myGame));
      selfdestruct(addr);
   }
}      

After the attack is executed as commented, the address(this).balance statement on the MyGame contract equals a number higher than the sum of the ether expected to be reached through the deposit method, effectively ending the game.

Since this function is payable, an easy way of doing this is sending the required ether in the attack call:

await myAttack.attack({value: ethers.utils.parseEther("6")});

If the attacker, instead of using selfdestruct, had used send or transfer, given that MyGame does not have receive nor fallback method, an exception would have been thrown.

Preventative Techniques

Don’t rely on address(this).balance:

contract MyGame {
   uint public targetAmount = 5 ether;
   address public winner;
   bool public prizeClaimed;
   uint public gameBalance;
   
   function deposit() public payable {
      require(msg.value == 1 ether, "You can only send 1 Ether");
      
      gameBalance += msg.value;
      require(gameBalance <= targetAmount, "Game is over");
      
      if (gameBalance == targetAmount) {
          winner = msg.sender;
      }
   }

   function claimPrize() public {
      require(msg.sender == winner, "You are not the winner");
      require(!prizeClaimed, "Prize already claimed");
      prizeClaimed = true;

      (bool sent, ) = msg.sender.call{value: gameBalance}("");
      require(sent, "Failed to send Ether");
   }
}

In this case, no matter how much ether is sent through an attacker’s self-destructing contract, the game will only keep track of funds sent through the deposit method, therefore ignoring the actual balance of the contract outside of ether received as part of the game.

Conclusion

Self destruct is a powerful feature in Solidity that can be useful for destroying a contract’s code and freeing up storage space on the blockchain. However, you should use it with caution and only when you are absolutely sure that you no longer need the contract. By following best practices and using self destruct sparingly, you can use this feature safely and effectively.

Posted in Blockchain, Ethereum, Smart Contract, SolidityTagged , ,

Martin Liguori
linkedin logo
twitter logo
instagram logo
By Martin Liguori
I have been working on IT for more than 20 years. Engineer by profession graduated from the Catholic University of Uruguay, and I believe that teamwork is one of the most important factors in any project and/or organization. I consider having the knowledge both developing software and leading work teams and being able to achieve their autonomy. I consider myself a pro-active, dynamic and passionate person for generating disruptive technological solutions in order to improve people's quality of life. I have helped companies achieve much more revenue through the application of decentralized disruptive technologies, being a specialist in these technologies. If you want to know more details about my educational or professional journey, I invite you to review the rest of my profile or contact me at martin@infuy.com