Let's take a look into the 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];
}
}
This is the basic structure of an ERC20 token
We have:
- a mapping to store the balance of each individual address who have tokens
mapping(address => uint) balances;
A uint256, which is used to track the total supply of the token
The constructor which set the initial supply and gives whoevers deploys the contract all the inicial supply
And a transfer function that allows us to transfer a specified amount of tokens to a specific address.
A balanceOf function which allows us to know how many tokens a specific address has.
The challenge
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.
Let's start thinking;
This transfer
function has a require
statement that checks whether the balance of tokens of whoever is calling the function, minus the _value
parameter, is equal or greater than zero.
This statement will always be true, since no matter what is the actual value evaluating on the left side it always will be greater or equal than zero. Remember that uint
means unsigned integer, those integers which don't have a sign, so they are always positive. So, we don't have to worry about this statement because it will always be true.
But, If balances[msg.sender] - _value
always would be a positive number, since those values are both uint256
, what would happen when balances[msg.sender]
is < _value
For example
Balances[msg.sender] = 25
Value = 40
25-40 = -15
So, WTF?????
This is when we find a great problem of older versions of solidity, in versions <.0.8.0 this problem is handle by default, overflow and underflow
Underflow & Overflow
As I said before the difference between two uint
is another unit
, so the result of an arithmetical operation like (30-31) instead of be -1, will be 2^256 - 1.
We can explain this with an example:
Let's say you have a clock, the typical clock with 12 items that goes from 1 to 12, let's call those points.
And let's imagine that you are in the 3rd position.
But we want to move forward 13th position, so we move from 3 to … oh we have a problem, we only have 12 positions, but 3 +14 is 16 WTF, Kevin >>:(
wait….
Okay , it's 16, but it's a clock. If we reach 12, we can start counting again from it. So, we can start moving from 3 to 12
And here we already moved 9 positions, so 13 - 9 = 4, we still have to move 4 positions, so we can simply start again from 12
And that's it, you are in the 4 position now.
NOW, imagine that clocks as a uint12
(take into account that, in reallity, that number defines the total of bytes that can be stored, but here we are using just the normal number, I’m not referencing bytes)
A unit that can only store 12 positions, but you want to store 16 positions, like in the example, what would happen? It happens that the variable will restart once it takes the upper bound and start again from 1 to add the rest of the numbers; we can call this an -overflow, because we are storing a quantity that is unhandle for that variable.
Back to our example; now let's imagine that instead of add, we subtract 13 positions, so 3 - 13 = -10, but in our clock example would be equal to 2, (here we are counting opposite clockwise, since we are subtracting).
in our uint12
, since it starts at 1, and there is no such a thing like cero (let's imagine) the only thing that could do this variable is start counting from 12 to one (once the lower bound is reached), we can call this an underflow, because we are storing a quantity that cannot be computed since is negative, and the only thing that left is start from the last number to the first (12 - 1)
So, this is why the required statement will always be true, because it is suffering from this underflow problem.
Back to the challenge, now the balances are stored in a uint256
(remember that uint
is 256 bytes by default). So there can be stored a really huge number stored there.
Then, we just have two simple arithmetic operations
one that subtracts the _value
we passed as parameter from our token account balance
and another that adds that amount to the _to
address you specified as parameter during the invocation of the function.
balances[msg.sender] -= _value;
balances[_to] += _value;
The subtract operation could also suffer an underflow
Since we are subtracting _vaue
from our balance, we can underflow that operation to allow us to steal a big, a really big amount of tokens. Something like
_value = 21
And since our balance is 20 tokens
20 - 21 will provoke an underflow, and will give to us a really huge amount of tokens (2**256-1)
Hacking
So let's write our script, you can follow the structure from the previous post.
ACCOUNT = accounts.add(config["wallets"]["from_key"])
ADDRESS_TO_SEND = "0x468F2866Cd6aEcf640644885F6Cad63Ff4f9BC4c"
def hack():
# grab the Token contract Object
instance_contract = interface.IToken(INSTANCE_ADDRESS)
# hack the contract
tx = instance_contract.transfer(ADDRESS_TO_SEND, 21, {"from": ACCOUNT})
tx.wait(1)
print(f"Contract has been hacked. Now we have {instance_contract.balanceOf(ACCOUNT)} tokens")
def main():
hack()
And that's it, we hacked the contract. :)
Now you can submit the contract instance, you can follow the steps from our previous post.
But hey! Could I overflow it? You could, in this case the third line of code will be affected balances[_to] += _value;
since you are using addition, you could do something like 2**256 + 21, that should give you an overflow. If you are using the brownie framework it will throw you an error
How can we prevent this problem?
Underflow and Overflow problems are handle by default in the latest versions of solidity, since version 0.8.0, but is always recommended that you check for those problems to avoid unexpected behaviors, you can use the safeMath library of OpenZeppelin that prevents this kind of problem in your arithmetic operation)
You can grab the complete solution in this github repo
If you have a comment or a suggestion please leave it in the section comments, also if you see any problem with the code feel free to make PR
You can follow me on my twitter @_bravoK and DM me, I’m always happy to talk and know more people of this amazing community
Stay tuned for the next Ethernaut solution.