A “smart contract” account is simply a program that runs on the Ethereum blockchain and is defined by its code (the functions) and data (its state).
Smart contracts are not controlled by a user; instead, they are deployed to the network and run according to their programming. Users can then interact with a smart contract by submitting transactions that execute a function defined on the smart contract. The interactions with smart contracts are irreversible.
Once deployed on the network, smart contracts cannot be changed or deleted by default. However, it is possible to include a selfdestruct capability that would allow a smart contract to be “deleted,” erasing the code and its internal state (storage) and leaving a blank account.
So, using the selfdestruct functionality enables developers to remove smart contracts from the Blockchain. But what are the reasons behind this drastic countermeasure?
To answer this question, it is helpful to read Why Do Smart Contracts Self-Destruct?
In this study, 340 self-destructing contracts were discovered and analysed. By defining self-destructed contract as a “Predecessor” contract, and its upgraded version as a “Successor” contract, the authors identified five primary reasons why contracts were destroyed by comparing the difference between the “Predecessor” contract and the “Successor” contract.
They discovered that the need for functionality changes is at the top of the list, followed by the case when a contract contains a bug or exhibits undesirable behaviour.
Furthermore, Ethereum provides an incentive to remove smart contracts from the Blockchain. This transaction has a negative gas cost because the operation frees up space on the Blockchain by clearing all of the contract’s data. This negative gas deducts from the total gas cost of the transaction.
The smart contract life begins with a creation transaction from an EOA or a contract account. The data payload of a transaction creating a smart contract is itself bytecode that runs the contract constructor (if present) to initialize the contract’s state.
On the other end, the final breath of a contract is its destruction. selfdestruct in Ethereum is an OPCODE at the EVM level, regardless of the language or client used.
This opcode is available as a high-level built-in function called selfdestruct , which only accepts one argument: the address to which any remaining ether in the contract account has to be sent.
selfdestruct(address recipient);
An important note is that a developer must explicitly add this command to the contract. Otherwise, the smart contract will never be deletable.
After the contract’s destruction, any transactions sent to that account address do not result in any code execution because there is no longer any code there to execute.
Deleting a contract does not remove the transaction history (past) of the contract since the blockchain itself is immutable.
“So using selfdestruct is not the same as deleting data from a hard disk.”
Now, we will create a straightforward smart contract with a constructor and a selfdestruct command:
Usually, it’s desirable that the selfdestruct command has to be callable only by the account that originally created the contract. For this reason, the address type variable named owner is used. In fact, at contract creation, the constructor assigns this variable to the msg.sender .
The contract has a minimal destroy() function. A require statement ensures that only the owner can run this function. It checks that the owner is the caller, otherwise fails. If the check is ok, the contract will self-destruct and send any remaining balance to the owner’s address.
The following tests show the intended behaviour of a destructible contract.
There is a common misconception about smart contracts. In fact, many inexperienced Solidity developers mistakenly believe that a contract can only obtain ethers through payable functions.
As seen before, any contract that implements the selfdestruct function can send all its ethers to a specific address. If the address is a contract address, the transfer of ethers occurs without calling any functions (including the fallback).
So, the selfdestruct function is a way to forcibly send ethers to any contract regardless of any code implemented inside it (even if the contract lacks payable functions).
This behaviour implies an attacker can easily force ether to be sent to a target contract in three simple steps:
This vulnerability leads to false assumptions about the actual ether balance of a contract.
There is nothing better than an example game contract to clarify how this situation can lead to unwanted effects.
This contract represents a simple game and is not intended to be used in practice. A user who wants to play sends 0.5 ether to the contract (calling the play() function). They hope to be the one that reaches one of three milestones. Milestones are determined amounts of ethers owned over time by the contract. The first user to reach a milestone can claim a reward in ether when the game has ended. The game ends when the final milestone is reached.
An attacker could send a small amount of ether (also 0.000001 is sufficient) via the selfdestruct function of a contract deployed by himself. This simple operation prevents any future player from reaching a milestone. This is because the currentBalance , as calculated at line 17, will never be a multiple of 0.5 ether thanks to this small ether contribution to the contract balance (note that players are forced to send only 0.5 ether increments).
The problem here is the poor use of this.balance at line 17. Contract logic should avoid being dependent on the contract’s balance as it can be maliciously manipulated.
A possible solution could be to use a self-defined variable (e.g. depositedEth ) and increment it to track the deposited ether safely. This variable will not be influenced by a selfdestruct call because it no longer references this.balance .
uint currentBalance = depositedEth + msg.value;
Thank you for reading. I hope you enjoyed the article.
Let me know what you think about it.