🪙 Token - The Underflow Glitch
Challenge Overview
In this level, we are given a basic token contract and start with a balance of 20 tokens. Our goal is to acquire a large number of additional tokens.
The Catch: We only have 20 tokens, and we cannot mint or purchase more through standard avenues.
Technical Analysis
Vulnerability: Arithmetic Underflow in Legacy Solidity
The Token contract is compiled with Solidity ^0.6.0 and implements a standard token transfer:
function transfer(address _to, uint256 _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true;}The Root Cause
Two critical flaws are present here:
- Solidity version <
0.8.0: Prior to version0.8.0, arithmetic operations in Solidity did not revert on overflow or underflow. Instead, they wrapped around. - Unsigned Integer Wrapping:
balances[msg.sender] - _valueusesuint256. Unsigned integers cannot represent negative numbers. If we attempt to subtract a value larger than our current balance (e.g.,20 - 21), the result underflows and wraps around to .
Because the expression underflows:
- The check
balances[msg.sender] - _value >= 0always evaluates totrue(since anyuint256value, including the wrapped underflow, is always ). balances[msg.sender] -= _valueunderflows our balance to an astronomical value near .
The Exploit: The Multi-Step Solution
- Identify a Target: Select any address other than ours (e.g., the contract’s address or a random address).
- Perform the Underflow Transfer: Call
transfer(target, 21)from our account (which has 20 tokens). - Inspect the Wealth: The subtraction
20 - 21underflows, and our balance becomes .
Proof of Concept (PoC)
Below is the JavaScript console commands to execute in the Ethernaut console:
// Transfer 21 tokens to a dummy address (more than our current 20)const dummyAddress = "0x0000000000000000000000000000000000000000";await contract.transfer(dummyAddress, 21);
// Verify our new astronomical balanceconst newBalance = await contract.balanceOf(player);console.log("Player's balance:", newBalance.toString());Auditor’s Lessons
- Upgrade Your Compiler: Always use modern Solidity versions (>=
0.8.0) where arithmetic overflows and underflows revert automatically by default. - Use SafeMath for Legacy Code: If you must audit or maintain legacy contracts (compiled with <
0.8.0), ensure all mathematical operations are processed using OpenZeppelin’sSafeMathlibrary. - Flawed Assertions: Avoid checking if an unsigned integer is greater than or equal to 0 (e.g.,
uint >= 0), as this statement is tautological and always returnstrue.
Remediation
The simplest and most secure fix is upgrading to Solidity 0.8.0 or higher, which handles checks natively:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;
contract SecureToken { mapping(address => uint256) public balances; uint256 public totalSupply;
constructor(uint256 _initialSupply) { balances[msg.sender] = totalSupply = _initialSupply; }
function transfer(address _to, uint256 _value) public returns (bool) { // Underflow will automatically revert here in solidity >= 0.8.0 require(balances[msg.sender] >= _value, "Insufficient balance"); balances[msg.sender] -= _value; balances[_to] += _value; return true; }
function balanceOf(address _owner) public view returns (uint256 balance) { return balances[_owner]; }}