Writeup for Naught Coin
- Hello h4ck3r, welcome to the world of smart contract hacking. Solving the challenges from Ethernaut will help you understand Solidity better. Each challenge involves deploying a contract and exploiting its vulnerabilities. If you’re new to Solidity and haven’t deployed a smart contract before, you can learn how to do so using Remix here.
Challenge Description
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
- The ERC20 Spec
- The OpenZeppelin codebase
Contract Explanation
If you understand the contract, you can move on to the exploit part. If you’re a beginner, please read the Contract Explanation to gain a better understanding of Solidity.
Click to view source contract
1
2// SPDX-License-Identifier: MIT
3pragma solidity ^0.8.0;
4
5import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
6
7contract NaughtCoin is ERC20 {
8 // string public constant name = 'NaughtCoin';
9 // string public constant symbol = '0x0';
10 // uint public constant decimals = 18;
11 uint256 public timeLock = block.timestamp + 10 * 365 days;
12 uint256 public INITIAL_SUPPLY;
13 address public player;
14
15 constructor(address _player) ERC20("NaughtCoin", "0x0") {
16 player = _player;
17 INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
18 // _totalSupply = INITIAL_SUPPLY;
19 // _balances[player] = INITIAL_SUPPLY;
20 _mint(player, INITIAL_SUPPLY);
21 emit Transfer(address(0), player, INITIAL_SUPPLY);
22 }
23
24 function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {
25 super.transfer(_to, _value);
26 }
27
28 // Prevent the initial owner from transferring tokens until the timelock has passed
29 modifier lockTokens() {
30 if (msg.sender == player) {
31 require(block.timestamp > timeLock);
32 _;
33 } else {
34 _;
35 }
36 }
37}
The NaughtCoin
contract inherits the ERC20
contract, which provides a standard implementation for creating tokens and assets. Before the introduction of the ERC20 token standard, different crypto assets or tokens used different logic, making it difficult to achieve interoperability between them. The ERC20 standard defines a set of functions that every token must have, enabling seamless interoperability between different tokens. This standardization has greatly simplified token transfers and interactions within the Ethereum ecosystem.
Click the below links to read more about the ERC20 standard.
- ERC-20: Token Standard
- ERC20 Token standard Basics
- ERC20 Token Standard Explained Must Watch
- ERC20 Contract
I assume you have gone through these resources and I will proceed.
1constructor(address _player) ERC20("NaughtCoin", "0x0") {
2 player = _player;
3 INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
4 _mint(player, INITIAL_SUPPLY);
5 emit Transfer(address(0), player, INITIAL_SUPPLY);
6}
During the deployment of the NaughtCoin
contract, the constructor will be called. The constructor takes an argument of type address as input. The ERC20
contract constructor takes two arguments as input: the token name and the token symbol. For the ERC20 contract constructor, “NaughtCoin” and “0x0” are passed as arguments. For the NaughtCoin contract, the address of the player (our address) is passed as an argument.
The constructor()
will set the player to the address passed as an argument. INITIAL_SUPPLY
is the initial supply of the tokens, which is set to 1000000 * (10 ** uint256(decimals()))
. The decimals()
function in the ERC20
contract returns 18
.
Then it will mint the initial supply of tokens to the player. Minting is the process of creating or issuing tokens. Finally, it will emit the Transfer event in the ERC20
contract.
1modifier lockTokens() {
2 if (msg.sender == player) {
3 require(block.timestamp > timeLock);
4 _;
5 } else {
6 _;
7 }
8}
The lockTokens()
is a modifier that restricts the transfer of tokens owned by the player for 10 years.
1function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {
2 super.transfer(_to, _value);
3}
The transfer()
function takes two arguments: the recipient’s address and the amount of tokens to transfer. It then executes the lockTokens()
modifier. If the modifier check passes successfully, the function will invoke the transfer()
function in the ERC20
contract.
Exploit
Our goal is to transfer all the tokens given to us and make our balance zero.
However, when we check the transfer()
function in the NaughtCoin
contract, it only allows us to transfer our tokens after a 10-year lockout period. Waiting for 10 years to transfer all our tokens and solve this challenge is not feasible. Therefore, we need to find a different method to transfer our tokens.
If we examine the ERC20 contract, we can find that there are two ways of transferring tokens. One method is directly transferring tokens by the owner, and the other method is allowing another account to spend tokens on behalf of the owner. If you haven’t reviewed the ERC20 contract, I strongly recommend doing so. You can find the contract here.
1function transfer(address to, uint256 value) public virtual returns (bool) {
2 address owner = _msgSender();
3 _transfer(owner, to, value);
4 return true;
5}
6
7function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
8 address spender = _msgSender();
9 _spendAllowance(from, spender, value);
10 _transfer(from, to, value);
11 return true;
12}
13
14function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
15 uint256 currentAllowance = allowance(owner, spender);
16 if (currentAllowance != type(uint256).max) {
17 if (currentAllowance < value) {
18 revert ERC20InsufficientAllowance(spender, currentAllowance, value);
19 }
20 unchecked {
21 _approve(owner, spender, currentAllowance - value, false);
22 }
23 }
24}
There are two ways of transferring tokens. However, if we look at the second method, there is a function called _spendAllowance()
. The _spendAllowance()
function checks whether the owner has allowed another account to spend tokens on their behalf. If there is no allowance, it will revert.
If we check our NaughtCoin
contract, we can see that it has only overridden and implemented the transfer()
function. This means that only the transfer()
function has the time lock. However, if we send tokens using transferFrom()
, we can instantly send the tokens without any restrictions. But before we can do that, we need to allow another account to spend tokens on our behalf, and then they can transfer the tokens to any other person. We can even approve our externally owned account and directly use transferFrom()
.
1function approve(address spender, uint256 value) public virtual returns (bool) {
2 address owner = _msgSender();
3 _approve(owner, spender, value);
4 return true;
5}
The approve()
function approves the spender to spend a certain amount of tokens on behalf of the owner. Now that we know what to do, let’s exploit this contract!
Now it’s time to open the console. Open the Naught Coin challenge and press Ctrl
+Shift
+J
to open the console. Enter the following commands:
1> await contract.approve(player, contract.INITIAL_SUPPLY)
1> await contract.transferFrom(player, address(0), contract.INITIAL_SUPPLY)
1> await contract.balanceOf(player)
If all the calls are successful, the balance will return zero, and now you can submit the level instance.
Key Takeaways
If we are implementing a normal token contract that works the same for everyone, we can directly inherit the ERC20 contract and use all its functions without overriding them.
However, if we are implementing a token contract that restricts certain users from withdrawing or depositing tokens, we need to override all the functions in the ERC20 contract and implement the restriction logic in those functions. If we don’t override the functions then whenever the user calls the function it will be directly called in the ERC20 contract and it will be executed normally because in the ERC20 standard contract there won’t be any restrictions to anyone.
***Hope you enjoyed this write-up. Keep on hacking and learning!***
Comments