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
- 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!