Problem statement
The Ethernaut is a Web3/Solidity based wargame inspired by overthewire.org, played in the Ethereum Virtual Machine.
This level, Hello Ethernaut, walks you through the very basics of how to play the game.
Solution
Since this level is all about to learn the platform it's pretty straight forward. Here is the gist of it:
- Install the MetaMask extension
- Get some Ether, this faucet (I'm using the Sepolia network) worked for me, which is provided by Alchemy
At the ethernaut page connect the MetaMask wallet and open up the developer tool in your browser (F12). Call the help method in the console window:
help()
Output:
(index) | Value |
---|---|
player | 'current player address' |
ethernaut | 'main game contract' |
level | 'current level contract address' |
contract | 'current level contract instance (if created)' |
instance | 'current level instance contract address (if created)' |
version | 'current game version' |
getBalance(address) | 'gets balance of address in ether' |
getBlockNumber() | 'gets current network block number' |
sendTransaction({options}) | 'send transaction util' |
getNetworkId() | 'get ethereum network id' |
toWei(ether) | 'convert ether units to wei' |
fromWei(wei) | 'convert wei units to ether' |
deployAllContracts() | 'Deploy all the remaining contracts on the current network.' |
Looks good, everything seems to be working so far. Next step is to create a new contract instance by clicking on the button, Get new instance. This will result in some feedback in the console window. Wait until an instance address has been returned.
Lets do a quick inspection of the contract object by just typing in the variable name in the console and push enter:
contract
/*
Expand the n {methods: {…}, abi: .....} row to see all
available methods/properties on the contract object:
* abi
* address
* allEvents
* authenticate
* call
* constructor
* contract
* estimateGas
* getCleared
* getPastEvents
* info
* info1
* info2
* info42
* infoNum
* method7123949
* methods
* password
* send
* sendTransaction
* theMethodName
* transactionHash
* [[Prototype]]
*/
Read through the list of methods/properties. Now you can start to interact with the contract object as the given instructions ask you to do.
await contract.info()
// Output: 'You will find what you need in info1().'
await contract.info1()
// Output: 'Try info2(), but with "hello" as a parameter.'
await contract.info2('hello')
// Output: 'The property infoNum holds the number of the next info method to call.'
await contract.infoNum()
// Output: { length: 1, negative: 0, red: null, words: [42, ] }
// Since the object has the number 42 in one of its properties,
// I guess that we should call the method info42 because
// it was also listed when we inspected the contract object earlier
await contract.info42()
// Output: 'theMethodName is the name of the next method.'
await contract.theMethodName()
// Output: 'The method name is method7123949.'
await contract.method7123949()
// Output: 'If you know the password, submit it to authenticate().'
// The contract object has a password method on it, lets
// call that one to see if it will return the password
await contract.password()
// Output: 'ethernaut0'
// Could be the password we are looking for
await contract.authenticate('ethernaut0')
// Output: ...sending transaction
// Nice
The final step is to click on the Submit button on the page to submit the solution back to ehternaut. This will result in log messages of your success in the console window.
Explanation
This is a web3 site which means in this case that the user will need a wallet to enable all features on the site. For example the button, Get new instance, will only be available if the wallet is connected.
The developers of the page has provided us with some objects and help functions which are mentioned in the description, however by using following code in the console you can list all objects available on the page:
console.table(Object.keys(window))
From the output you can see the objects that are important for this level, contract and help.
Before the creation of a new instance of the contract it will be initialized to the string No contract set, go to a level and click 'Get new instance'.
When clicking on the button, Get new instance, we will start to interact with the Ethernaut contract on Ethereum (Sepolia network in this case) and call the function createLevelInstance. The call will deploy a new contract and therefore the need of Eth.
The address of the Ethernaut contract can be found in the console:
ethernaut
// Output: An Ethernaut contract object which has
// a property with the contract address on the blockchain
The input data to createLevelInstance is an address, 0x7E0f53981657345B31C59aC44e9c21631Ce710c7 which we also can look up on the etherscan. That input tells which contract to deploy for this level and the creator of the contract will be the wallet that is connected to the site.
By looking at the Internal Transactions for the contract 0x7e0f...10c7, we can see details about the contract creation. Here it is possible to see what is given to the contract constructor.
Looks like the password is given when creating the contract.
There will be some feedback when the deployment of the contract is done in the console window, and the contract object will be initialized with the deployed contract. This contract will also have an address that can be used to look up the contract on etherscan. However the code given when passing the challenge looks better than the decompiled version at etherscan.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Instance {
string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;
// constructor
constructor(string memory _password) {
password = _password;
}
function info() public pure returns (string memory) {
return 'You will find what you need in info1().';
}
function info1() public pure returns (string memory) {
return 'Try info2(), but with "hello" as a parameter.';
}
function info2(string memory param) public pure returns (string memory) {
if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
return 'The property infoNum holds the number of the next info method to call.';
}
return 'Wrong parameter.';
}
function info42() public pure returns (string memory) {
return 'theMethodName is the name of the next method.';
}
function method7123949() public pure returns (string memory) {
return 'If you know the password, submit it to authenticate().';
}
function authenticate(string memory passkey) public {
if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}
function getCleared() public view returns (bool) {
return cleared;
}
}
As you can see the query methods are pure which means that we don't need to pay any fees.
When we finally call the authenticate function we are updating the state of the contract (update of the cleared variable if the given password is correct) and therefore a new transaction is done to the network.
Last action is the interaction with the Ethernaut contract again which submits the instance so it can keep track of the progress. Also this can be tracked in Etherscan.
At the end it should be three activities registered in your wallet.
Resources
- OpenZeppelin - The creator of the CTF
- The Ethernaut - Main page for the Ethernaut CTF
- Hello Ethernaut - This challenge
- MetaMask - Crypto wallet extension
- Sepolia faucet - Faucet to retrieve Eth
- Alchemy - Provider of the Sepolia faucet
- Ethernaut contract - The Ethernaut contract on etherscan
- Etherscan - is your friend