Emanuele Ricci
Ethernaut Challenge #5 Solution — Token
This is Part 5 of the "Let's play OpenZeppelin Ethernaut CTF" series, where I will explain how to solve each challenge.
The Ethernaut is a Web3/Solidity based wargame created by OpenZeppelin. Each level is a smart contract that needs to be 'hacked'. The game acts both as a tool for those interested in learning ethereum, and as a way to catalogue historical hacks in levels. Levels can be infinite and the game does not require to be played in any particular order.
Challenge #5: Token
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.
Level author: Alejandro Santander
We start with a balance of 20 Token and to solve the challenge we need to gain at least 1 more token, but we will try to gain much, much more ;)
Study the contracts
The Token
contract is a simplified and stripped down version of an ERC20 Token.
The contract has these state variables:
mapping(address => uint256) balances
to map user balancesuint256 public totalSupply;
to track the total supply. The total supply could have been declared asimmutable
because is only initialized in the contract, and it is never updated.
Then we have the constructor method constructor(uint256 _initialSupply) public
where the creator of the contract mint _initialSupply
token updating the totalSupply
and his/her balance to that value
We see two other function
function balanceOf(address _owner) public view returns (uint256 balance)
that simply returns the balance of the specified_owner
addressfunction transfer(address _to, uint256 _value) public returns (bool)
that should transfer_value
of tokens from themsg.sender
to the_to
address.
Well, as you might think, probably the problem of this contract will be in that specific function. Let's review its code:
function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
Everything seems fine, right?
- check if the sender has enough balance to make the transfer
- update the sender balance
- update the receiver balance
- return true
Have you spotted the problem? I have already highlighted it in the previous blog post and in this one I just waited to arrive at this point to tell you about it!
The contract uses Solidity 0.6.0, but it is not using a library like SafeMath to handle under/overflow!
Let's make an example on how underflow work:
- Alice has a balance of
balances[alice] == 20
- Alice call
transfer(Bob, 21)
- The check
balances[msg.sender] - _value
done byrequire
insidetransfer
will result in an underflow. The result of the operation isuint256(-1)
that is equal to(2**256) – 1
. Usually with Solidity >0.8 or with SafeMath that operation would result in a revert - Because of the underflow, even if Alice does not own
21
tokens, they pass the check and the smart contract proceed with the balance update balances[alice] = 20 - 21 = (2**256) – 1
balances[bob] += 21
Side note: as we said, the transfer
method suffer from the under/overflow problem. This mean that an attacker could also break the balance of a user completely, resetting it!
If bob
has balances[bob] = (2**256) – 1
(equal to the max uint256
value), Alice
could make just a transfer(bob, 1)
and the new balances[bob]
would be 0.
Solution code
The solution is pretty straightforward:
function exploitLevel() internal override {
vm.startPrank(player, player);
// our balance is of 20 tokens
// because the contract suffer of underflow this operation
// will make our new balance equal to the max `uint256` value!
level.transfer(address(levelFactory), 21);
vm.stopPrank();
}
You can read the full solution of the challenge opening Token.t.sol
Further reading
- SWC-101: Integer Overflow and Underflow
- Consensys Ethereum Smart Contract Best Practices: Insecure Arithmetic
- Solidity v0.8.0 Breaking Changes: Arithmetic operations revert to underflow and overflow
Disclaimer
All Solidity code, practices and patterns in this repository are DAMN VULNERABLE and for educational purposes only.
I do not give any warranties and will not be liable for any loss incurred through any use of this codebase.
DO NOT USE IN PRODUCTION.