Ethernaut 5: Token

My solution for the fifth ethernaut challenge: https://ethernaut.openzeppelin.com/level/0x63bE8347A617476CA461649897238A31835a32CE

Investigation

We have a Token contract that contains balances for address and a total supply.

mapping(address => uint) balances;
uint public totalSupply;

We have a constructor which initializes a contract instance with a provided supply and gives that entire balance to the contract deployer

constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
}

There is a function which allows a transfer from the senders balance to the provided address. It simply checks that there is enough of a balance to be sent in the senders address, deducts the amount from the sender and assigns it to the to address.

function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
}

Notably here, the computation is being done on uint values which can be overflowed. I tried out the transfer function using the Remix IDE and found that I was able to transfer 5 tokens to some other address. This worked since 5 was less than the amount in my wallet (initialized with 20 tokens).

Solution

First, I tried to pass in a negative value to the function. My thinking is that the value would be treated as a uint during smart contract execution, which would be a very large number. This approach does not work though and I got an error from the EVM

instance.transfer("0x...", -10);
// Error encoding arguments: Error: value out-of-bounds (argument=null, value="-5", code=INVALID_ARGUMENT, version=abi/5.5.0)

So it seems like it wont work to just try passing in a negative number. Thinking about it some more, I noticed that on this line

require(balances[msg.sender] - _value >= 0);

An integer overflow can also be induced. ie. if I try to send more than I have in my wallet, balances[msg.sender] - _value will actually overflow and return some large value. I tried this out

instance.transfer("0x...", 25);
instance.balanceOf("<my address>");
// uint256: balance 115792089237316195423570985008687907853269984665640564039457584007913129639931

My wallet now has several trillions of tokens 🤠

What I learned

  1. During the investigation, I found that this will actually not be an issue using a newer solidity version to compile our code (0.8.0 and above).

    Arithmetic operations revert on underflow and overflow. You can use unchecked { ... } to use the previous wrapping behaviour.

    From https://docs.soliditylang.org/en/latest/080-breaking-changes.html

  2. This issue can be prevented on lower solidity versions with OpenZeppeling SafeMath.