Solidity Types — Ethernaut Level 5 Tokens

Kevan Mordan
DraftKings Engineering
3 min readNov 4, 2022

Solidity requires all variables to have an explicitly declared type. There is not a generic var that is left to the compiler to determine. Type conflicts or misusing types can lead to nasty consequences.

Overflow and Underflow

One of the most common vulnerability types is unsigned integers (uints). Unsigned integers are named so because they can represent positive values. Signed integers reserve the leading bit to indicate the sign (positive or negative) whereas uint’s use the leading bit as part of the numerical value. Solidity defaults uint to 256 bits and therefore has a range from 0 to (²²⁵⁶ -1).

A common uint exploit is to take advantage of overflow or underflow. The uint value is like an odometer, once it reaches its max value (i.e. 999,999 miles) it actually resets back to 0. And if Ferris Bueller’s driving in reverse actually worked to take miles off the car, starting at 0 miles would decrement to 999,999 miles (there are no negative values to go down to!). If not properly handled, attackers can pass different numerical values to trigger under/over flows to bypass system restrictions.

How to Prevent Them

The good news is that modern Solidity (version 0.8.0+) handles both underflow and overflow automatically. The function will revert and throw an error. If you happen to be locked into an earlier version, we recommend OpenZeppelin’s SafeMath to provide the same functionality. In fact, this is used in other Ethernaut levels!

Ethernaut Level 1 Fallback SafeMath

The Problem

https://ethernaut.openzeppelin.com/level/0x63bE8347A617476CA461649897238A31835a32CE

The level instance is instantiated with the player address having a balance of 20 tokens. The goal is simply to get any more than 20 tokens. The more tokens the better, but 21 tokens beats the level the same as 21,000,000 tokens.

The level only has one function that can change an account’s token balance, transfer. The other function, balanceOf, is a read only (view) function that simply returns an address’s balance.

Let’s break down the transfer function:

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

Remember that _value and balances[msg.sender] are both uints. Because of underflow, a uint value can never be negative and this required condition will always pass, no matter the values.

balances[msg.sender] -= _value;
balances[_to] += _value;

Ok, now we just need to figure out how to give our address more tokens. Because the same addition and subtraction calculations are being used, we need to transfer to a different address. Otherwise the operations will cancel out. That means we need to pass a value that when subtracted from 20 will result in a number that is greater than 20. Now, with proper validation or using signed integers, this would be impossible, but we can leverage underflow to set our balance to a value greater than 20.

Back to the same odometer example, if decrementing 1 from 0 results in the maximum possible value, then decrementing 21 from 20 will also result in the maximum possible value. Calling transfer to any address (we will just use the instance address) with a value of 21 will indeed leave us with the max value of tokens that uint can support.

The next problem, Ethernaut Level 6, will focus on delegating calls.

Let’s head to the browser console to solve this level:

  • First, check the initial balance to verify it is indeed 20.
Check initial player balance
  • Second, submit a transfer function to the instance address with a value of 21.
Transfer function
  • Third, check the player balance again to determine that it is indeed very large!
Ending player balance

Submit!

--

--