Solidity Attack Vectors: #7 Force Feeding

Solidity Attack Vectors: #7 Force Feeding

Solidity is a contract-oriented programming language that has the ability to hold value (Ether). Many smart contracts deal with value, such as for savings, lending, trades, or other concepts related to value. This brings financial freedom due to the decentralized and permissionless nature of the blockchain.

A contract can hold a large amount of Ether, depending on its purpose of creation. These values can belong to users of the smart contract, and you wouldn't want anything to happen to them. If the balance of users in the contract is meant to be easily accessed by the users, it should be, no matter what.

Force Feeding and Disruption of Accounting Systems

One thing to be careful about with any smart contract that holds value is its accounting system. Any mistake in your accounting can lead to problems.

Force feeding is an attack that affects the accounting system of a smart contract by sending Ether value forcefully into the contract. It is generally recommended to avoid using a smart contract's balance for strict equality checks, as they can be affected through a force feeding attack.

Now let's consider the following code:

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

contract OneEtherGame {
    uint256 public maxBalance = 3 ether;
    address public winner;

    function play() external payable {
        require(msg.value == 1 ether, "Send one ether");
        uint256 contractBalance = address(this).balance;

        require(contractBalance <= maxBalance, "Game over");
        if (contractBalance == maxBalance) {
            winner = msg.sender;
        }
    }

    function claimReward() external {
        require(msg.sender == winner, "Not winner");
        uint256 contractBalance = address(this).balance;
        require(contractBalance == maxBalance, "Game over");

        (bool success, ) = payable(msg.sender).call{
            value: address(this).balance
        }("");

        require(success, "Failed to send Ether");
    }

    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

You can call the "play" function in the "OneEtherGame" contract with 1 Ether until the max balance of 3 ether is reached. The contract checks if its balance is up to the max balance of 3 ether and sets the last address to the winner if it is. After the max balance is reached, the winner can now claim their reward.

We see that we are comparing the balance of the contract, which is stored in the local variable "contractBalance", to the "maxBalance" state variable. But remember that the smart contract balance can be affected by force feeding, as mentioned earlier, and this can affect its accounting system when using it to compare with the maximum balance. If the contract is force fed with a larger amount than the "maxBalance" variable, Ether can be stuck and the winner is never set.

Let's see how we can attack this contract by force feeding it.

Force Feeding A Smart Contract

An attacker can forcefully send Ether in three ways. They are as follows:

  1. Using the "selfdestruct" function: When the "selfdestruct" function is called in a smart contract, the bytecode and storage of the smart contract are destroyed, and the Ether inside the smart contract is forcefully sent to a specified address, whether the address accepts Ether or not.

  2. Deterministic Deployments: Smart contract addresses are the keccak hash of the rlp (recursive length prefix) encoding of the sender's address and nonce. You can send ether to a contract address even before it is deployed if you know it.

  3. Block rewards and coinbase: If an attacker is a miner, they can use the contract's address as their block coinbase and rewards (Ether) will be sent to the contract whether it can receive Ether or not.

How Do We Protect Our Smart Contract?

When it comes to force feeding attacks, we can implement good design patterns to prevent our contract's accounting system from being affected by the selfdestruct function, but we can't stop the selfdestruct function or other factors mentioned above. This is because both selfdestruct and the setting the coinbase of a block are carried out by the Ethereum Virtual Machine (EVM), so checks by smart contracts are ineffective. So, instead of using a smart contract balance for comparison and guards(require statements), other methods can be used.

We can rewrite the OneEtherGame contract in a way that prevents its internal accounting system from being affected by the selfdestruct function. Here is an example of how it could be done:

contract OneEtherGameTwo {
    uint256 public maxBalance = 3 ether;
    address public winner;
    uint256 public currentBalance;

    function play() external payable {
        require(msg.value == 1 ether, "Send one ether");
        currentBalance += msg.value;

        require(currentBalance <= maxBalance, "Game over");
        if (currentBalance == maxBalance) {
            winner = msg.sender;
        }
    }

    function claimReward() external {
        require(msg.sender == winner, "Not winner");
        require(currentBalance == maxBalance, "Game over");
        (bool success, ) = payable(msg.sender).call{value: currentBalance}("");
        require(success, "Failed to send Ether");
        currentBalance = 0;
        winner = address(0);
    }

    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

You can see in the contract above that we introduced some concepts. We now have a "currentBalance" variable that keeps track of how much Ether we have received from the "play" function. Since this is only set inside the "play" function, if our contract gets force-fed, it wouldn't be a problem because it doesn't affect the "currentBalance" variable that we are now using for guards and comparison.

Conclusion

It's important to note that most times smart contracts can not prevent being hit by a force-feeding attack, but with the necessary steps, you can prevent your internal accounting systems from being affected or messed up.

See you in the next article on Solidity Attack Vectors.