This is the series on playing with Ethernaut, a level-based solidity challenge focusing on the hands-on solidity/web3 hacking. And I like this for adding fun to learning. Thanks to Openzeppelin team.
Let's talk about the challenge
This challenge has a difficulty of 3/10. We start with 20 tokens and have to increase our balance from the initial value. Sound simple?
Here is the 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];
}
}
Link to play: https://ethernaut.openzeppelin.com/level/0x63bE8347A617476CA461649897238A31835a32CE
Challenge Analysis
To have our balance increased normally, we would need someone to send us a new token or to withdraw it from somewhere. But it does not seem to be possible with this token.
So our guess would be something about number overflow or underflow. The number overflow in Solidity can be really problematic.
Let's say we do this.
uint256 a = 10
uint256 b = 20
uint256 c = a - b // => 115792089237316195423570985008687907853269984665640564039457584007913129639926
What happened here? Why it is not -10
😱😱😱 ?
Prior to the Solidity version
0.8.x
, when a number in solidity goes above "max", it will roll back to "min" (overflow). And in the same manner, when a number goes below "min", it will roll to "max" (underflow).
In the above case, uint256
consists of non-negative numbers from
(min) to
(max).
And when it goes down below 0, it becomes the max number. So we get the result of max - 10
For example,
max_uint256 = 2 ** 256 - 1
uint256(-1) == max_uint256 // => true
uint256(-2) == max_uint256 - 1 // => true
uint256(-3) == max_uint256 - 2 // => true
max_uint256 + 1 == 0 // => true
max_uint256 + 2 == 1 // => true
max_uint256 + 3 == 2 // => true
One simple way to protect overflow is using SafeMath. However, since the version 0.8.x, the overflow/underflow protection has already been built-in.
Read more about overflow and underflow: https://ethereum-blockchain-developer.com/010-solidity-basics/03-integer-overflow-underflow/
Solving the challenge
So we would take a look at the code to see if there is any unprotected underflow.
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
We can see the require
line looks like something we can exploit. What if we make balances[msg.sender] - _value
goes lower zero? This would roll the number back to max and pass the require
condition.
Then on the next line balances[msg.sender] -= _value;
our balance will go underflow and become max as well.
Solution
Same as before, it is recommended for you to try first before going forward.
The solution is pretty straightforward, if we want to have max balance possible, we need to make our balance become -1
. And we start with 20, so we would want to transfer out 21.
// Put this in the browser console
// Any other address is ok
sendToAddress = contract.address
// In case we want any balance increased, any number more than 20 is good already.
sendAmount = 21
// call the transfer function with our evil params 😈
contract.transfer(sendToAddress, sendAmount)
And...... Bam, now we are really rich! 🤑🤑🤑
That's it! Feel free to share if you have any ideas or suggestions.
Photo by NIKHIL KESHARWANI on Unsplash
Top comments (0)