Problem statement
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?
// 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];
}
}
Solution
Start with creating a new contract for the current level by clicking on the button, Get new instance. Remember to have enough eth in the connected wallet and that it's connected to the Sepolia network.
Open up the developer tool in your browser (F12) and make a transfer, where _to
is equal to '0x95D34980095380851902ccd9A1Fb4C813C2cb639' (any valid address will work, and _value
equal to 21.
await contract.transfer('0x95D34980095380851902ccd9A1Fb4C813C2cb639', 21)
Check that you have more than 20 tokens.
await contract.balanceOf(player)
If the balance is more than 20, then finish up the challenge by clicking on the button, Submit instance, to commit and update the progress on the ethernaut contract.
Explanation
The Token contract only has one function that modifies the state and therefore the vulnerability of the contract probably lies in that 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;
}
As we can see there are only arithmetic operations made in this function, however these kind of operations are classical causes to software bugs. It boils down to how we represent numbers in computers and programming languages.
The input variable _value
and the mapping variable balances
use the type uint. This means that these variables can only represent numbers between 0 to (2^256)-1, inclusive, in simpler terms, "only positive values".
If you make an arithmetic operation between two uint's the result has to be of the type uint also.
This call to the transfer function
await contract.transfer('0x95D34980095380851902ccd9A1Fb4C813C2cb639', 21)
will look like this when substituting the values in the Solidity code.
// original code
require(balances[msg.sender] - _value >= 0);
// substitute the variables to actual values used
require(20 - 21 >= 0);
// after arithmetic calculation
require(115792089237316195423570985008687907853269984665640564039457584007913129639935 >= 0);
The expression 20 - 21
can't be -1
since the result has to be a positive number (uint), therefore an overflow is occurring and producing the big number, and there is the vulnerability.
It's important to know that this behavior has changed with recent version of Solidity (from v0.8.0), but this contract is on version
pragma solidity ^0.6.0;
so that's an important factor when looking for vulnerabilities in a contract. To prevent this problem in older contracts you may use SafeMath from OpenZeppelin.
The conclusion from the author of this level.