Writeup for Token
- Hello h4ck3r, welcome to the world of smart contract hacking. Solving the challenges from Ethernaut will help you understand Solidity well. For each challenge, they will deploy the contract and provide us with the instance of that contract. Our task is to interact with the contract and exploit it. Don’t worry if you are completely new to Solidity and have never deployed a smart contract before. You can learn how to deploy a contract using Remix here.
Challenge
- 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.
Contract Explanation
Click to view source contract
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.6.0;
3
4contract Token {
5 mapping(address => uint256) balances;
6 uint256 public totalSupply;
7
8 constructor(uint256 _initialSupply) public {
9 balances[msg.sender] = totalSupply = _initialSupply;
10 }
11
12 function transfer(address _to, uint256 _value) public returns (bool) {
13 require(balances[msg.sender] - _value >= 0);
14 balances[msg.sender] -= _value;
15 balances[_to] += _value;
16 return true;
17 }
18
19 function balanceOf(address _owner) public view returns (uint256 balance) {
20 return balances[_owner];
21 }
22}
If you feel like you understand the contract, you can move to the exploit part. If you are a beginner, please go through the Contract Explanation as well. It will help you understand Solidity better.
The contract has two state variables:
balances
andtotalSupply
.balances
is a mapping of address to tokens, andtotalSupply
is the total number of tokens available.
1 constructor(uint256 _initialSupply) public {
2 balances[msg.sender] = totalSupply = _initialSupply;
3 }
- In the above code snippet, the constructor takes an argument
_initialSupply
and sets the balances ofmsg.sender
andtotalSupply
to_initialSupply
.
1 function transfer(address _to, uint256 _value) public returns (bool) {
2 require(balances[msg.sender] - _value >= 0);
3 balances[msg.sender] -= _value;
4 balances[_to] += _value;
5 return true;
6 }
The function
transfer()
is a public function that takes two arguments address and amount to transfer as input, and returnstrue
if the transfer is successful.First, it checks if the balance of
msg.sender
is more than the value they are transferring. If yes, it will continue executing the next lines; otherwise, it will revert.If the require statement is satisfied, it will reduce the balance of
msg.sender
and increase the balance of_to
, and then returntrue
.
1 function balanceOf(address _owner) public view returns (uint256 balance) {
2 return balances[_owner];
3 }
- The function
balanceOf()
is a public function that takes an address as input and returns the balance of that address.
Exploit
The only function in the contract that changes the state of the contract is the
transfer()
function. So we need to look at thetransfer()
function for any loops.If we check the solidity compiler version, it is ^0.6.0, which means any version more than 0.6.0 is supported.
Solidity versions less than 0.8.0 don’t implicitly check for overflow and underflow errors. Let me give you a basic example of what overflow and underflow are. Observe the following contract.
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.6.0;
3
4contract overflow_underflow{
5 uint8 overflow=255;
6 uint8 underflow=0;
7
8 function increment()public{
9 overflow++;
10 }
11
12 function decrement()public{
13 underflow--;
14 }
15
16}
The above contract is a good example to understand overflows and underflows. The state variable
overflow
is set to 255, and the state variableunderflow
is set to 0.uint8
technically refers to 8 bits, which means it can store a maximum value of 255. If we increase the value of the variable after 255, it will start from zero again. In the above contract, if we callincrement()
once, thenoverflow
will be set to zero. If we call it again, it will be set to 1, and so on.The minimum value of
uint8
is 0. But if we decrease auint8
variable after zero, it will become 255. In the above contract, if we calldecrement()
once, thenunderflow
is set to 255. If we call it again,underflow
will be set to 254, and so on.For solidity versions greater than 0.8.0, overflow and underflows are implicitly handled. But for solidity versions less than 0.8.0, we need to explicitly handle the overflows and underflows.
There is a library named SafeMath to handle overflows and underflows for versions less than 0.8.0.
Initially, we were given 20 tokens. If we observe, when we transfer, it reduces our balance. If we transfer 20 tokens, we will have 0 tokens. But if we transfer 21 tokens, an underflow will occur, and our balance will be set to 2^255 - 1.
The require statement is also passed because
balances[msg.sender]
will return 20, and we are transferring21
. So again, an underflow will occur, and the value returned will be greater than 0.Now it’s time to open the console. Open the Token challenge and press
ctrl
+shift
+j
to open the console.
1> await contract.transfer("0x0000000000000000000000000000000000000000",21)
- That’s it! Once the transaction is completed, you can submit the instance of this challenge.
Key Takeaways
- For solidity versions less than 0.8.0, we need to explicitly handle the overflows and underflows. We can use the SafeMath library to overcome those.
***Hope you enjoyed this write-up. Keep on hacking and learning!***
Comments