paint-brush
What is a Self Destruct Attack in Smart Contracts and How to Prevent Them? 🚨🛡️by@codingjourneyfromunemployment
434 reads
434 reads

What is a Self Destruct Attack in Smart Contracts and How to Prevent Them? 🚨🛡️

by codingJourneyFromUnemploymentNovember 29th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Uncover smart contract vulnerabilities like self-destruct attacks & how to fortify against them. Dive into security measures for safer blockchain code!
featured image - What is a Self Destruct Attack in Smart Contracts and How to Prevent Them? 🚨🛡️
codingJourneyFromUnemployment HackerNoon profile picture

Self-destruct attacks in smart contracts fundamentally target the logic of the contract. Although these attacks are relatively straightforward, they remain a common threat in the realm of smart contract deployment. This highlights the importance of comprehensive logic considerations, extensive testing, and code audits before deployment. In this article, I'll showcase a simple example of a self-destruct vulnerability and how to counter such attacks. 🧠💻

The Process of a Self-Destruct Attack 🔄

Alice deployed a simple game contract. Its main function allows each user to deposit one ether at a time, with the 7th depositor winning and claiming all the ether in the contract as a reward. Here's the code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract EtherGame {
    uint public targetAmount = 7 ether;
    address public winner;

    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 claimReward() public {
        require(msg.sender == winner, "Not winner");

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

Alice and Bob each deposited 1 Ether, making the contract's total 2 Ether.


Then, Eve deployed an Attack contract, injecting it with five ethers. She then executed the attack() function to launch a self-destruct assault on the EtherGame contract. Here's the attack code:

contract Attack {
    EtherGame etherGame;

    constructor(EtherGame _etherGame) payable {
        etherGame = EtherGame(_etherGame);
    }

    function attack() public payable {
        // You can simply break the game by sending ether so that
        // the game balance >= 7 ether

        // cast address to payable
        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

Upon deploying the Attack contract, Eve used the address of Alice's EtherGame contract in the constructor. The line etherGame = EtherGame(_etherGame); in the constructor forcibly converts this address to an EtherGame contract type and assigns it to the etherGame variable. This essentially builds a reference to an EtherGame-type contract, pointing to Alice's deployed EtherGame contract.


Eve then invoked the Attack.attack function. The address payable addr = payable(address(etherGame)); line creates a memory reference to the etherGame contract address. The selfdestruct(addr) destroys the Attack contract and transfers the remaining five ether to this address.


The act of destroying the contract with self-destruct and sending five ether can bypass the logic and checks of other contracts when receiving ether transfers. It means it won't trigger the require(msg.value == 1 ether, "You can only send 1 Ether"); logic in the deposit() function, effectively making the EtherGame contract's balance ≥ 7 ether.


Consequently, no one can now win this game, nor can more ether be sent to it.

Preventing Self Destruct Attacks 🛡️🚫

This attack is simple but exploits a loophole in the EtherGame contract's logic. The idea is if I complete some operations without going through the contract's own logic or calling its functions, thus altering the contract's state, I can manipulate the game rules for profit.


Therefore, going back to the contract's own logic design, we should consider that the game rules and winner conditions need to be immune to self-destruct transfers.


In this case, the reason self-destruct could bypass the deposit() function is mainly because we used address(this).balance; to represent the current contract balance, also using this value as the condition for determining the winner. Self-destruct can change this value without invoking deposit().


The simplest way is to refine the contract's logic, using a data source that self-destruct cannot change without invoking deposit() as the standard for determining the winner. For example, add a state variable to the contract to replace the role of address(this).balance;:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract EtherGame {
    uint public targetAmount = 7 ether;
    uint public balance;
    address public winner;

    function deposit() public payable {
        require(msg.value == 1 ether, "You can only send 1 Ether");

        balance += msg.value;
        require(balance <= targetAmount, "Game is over");

        if (balance == targetAmount) {
            winner = msg.sender;
        }
    }

    function claimReward() public {
        require(msg.sender == winner, "Not winner");

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

Now, the EtherGame contract has turned into a honeypot. Attackers can still trigger Attack.attack(), activating self-destruct to send ether, but it only increases the game's prize without disrupting the game rules. 🎮💸


That's all about the self-destruct vulnerability and related security strategies. If you've made it this far, why not give a thumbs up? 👍💬🌟