Self-destruct can be your light at the end of the tunnel

November 26, 2022

Hey, in this article I'll show you the impact of self-destruct.

When an amount of ether is sent to a contract it must execute either the payable fallback function or another payable function defined in the contract, what if i told you that an contract can receive ether without having one of theses function types? Yes, it's possible due Self-destruct. Contracts that rely on code execution for all ether sent to them can be vulnerable to attacks where ether is forcibly sent.

Self Destruct Attack

This attack occurs when the contract make validations with the balance of the contract, and the balance of the contract should depend only on the user's deposit.

When this happens, an attacker can simply create an exploitation contract and use the selfdestruct function to destroy the exploitation contract and send all funds to the target contract, and thus change the contract balance.

Example

Here we have a contract of EtherGame, in this contract, the 7th user who makes a deposit becomes the winner of the game, and can withdraw his reward.

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

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");
    }
}

It is possible to circumvent the game by creating a new contract, depositing the 6 amount of ETH and calling the selfdestruct function, after this, just call deposit function depositing 1 ETH in EtherGame using a wallet.

contract Attack {
    function attack(address payable _targetAddress) public payable {
        selfdestruct(_targetAddress);
    }
}

Why Self-destruct can be your light at the end of the tunnel?

In some situations the contract you are auditing may have a function with require statements that check the balance of the contract and not have a payable fallback function, and using selfdestruct you can manipulate the balance of the contract in your favor.

How to prevent from this attack?

Contract logic should avoid being dependent on the balance of the contract, because it can be manipulated. If applying logic based on this.balance, you have to cope with unexpected balances.

If exact values of deposited ether are required, a defined variable should be used that is incremented in payable functions, to track the deposited ether. This variable will not be influenced by the forced ether sent via a selfdestruct call.

Using this knowledge, check bellow a secure version of EtherGame:

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

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

    function deposit() public payable {
        require(msg.value == 1 ether, "You can only send 1 Ether");
	    deposited += msg.value
        require(deposited <= targetAmount, "Game is over");

        if (deposited == 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");
    }
}