Ethernaut#5 - Token
Ethernaut #5 - Token
The goal of this level is for you to hack the basic token contract below. You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens.
Things that might help:
- What is an odometer?
1 | // SPDX-License-Identifier: MIT |
Contract Breakdown
Let us breakdown the contract to understand what each function and piece of code does. The code consists of a single contract Token compiled with solidity version ^0.6.0. Notice it is using an older version of Solidity, this is very important.
Variables
1 | mapping(address => uint) balances; |
A mapping balances that maps addresses to their token balance, and totalSupply which tracks the total amount of tokens.
Constructor
1 | constructor(uint _initialSupply) public { |
The constructor takes an _initialSupply and assigns all of those tokens to the deployer. It also sets the totalSupply to that same value.
Functions
1 | function transfer(address _to, uint _value) public returns (bool) { |
The transfer function allows a user to send _value amount of tokens to another address _to. It first checks that balances[msg.sender] - _value >= 0. Then it subtracts _value from the sender and adds it to the receiver.
1 | function balanceOf(address _owner) public view returns (uint balance) { |
A simple view function that returns the balance of a given address.
Solution
The vulnerability here is an integer underflow. This contract is compiled with Solidity ^0.6.0 which does NOT have built-in overflow/underflow protection (that was introduced in ^0.8.0) and the contract doesn’t use SafeMath either.
The
require(balances[msg.sender] - _value >= 0)check is useless becausebalancesis of typeuint(unsigned integer). An unsigned integer can never be negative, sobalances[msg.sender] - _valuewill always be >= 0. If the subtraction would result in a negative number, it wraps around to a very large number instead.
Think of it like an odometer on a car. If you have 20 on the odometer and you try to subtract 21, instead of going to -1 it wraps around to the maximum value: 2^256 - 2 which is an astronomically large number.
To pass this challenge, we don’t even need to deploy a hack contract. We can just interact directly through Remix or the console.
Steps to pass:
- Load the Token contract in Remix with the instance address from Ethernaut.
- Call
transferwith any address (that is not our own) as_toand21as_value. - We have 20 tokens. Transferring 21 triggers an underflow:
20 - 21wraps around to2^256 - 1which is a massive number. - Our balance is now an absurdly large amount of tokens.
This is why Solidity 0.8.0+ introduced automatic overflow/underflow checks that revert the transaction. For older versions, always use OpenZeppelin’s SafeMath library. Never perform arithmetic operations on unsigned integers without proper checks in pre-0.8.0 contracts.