Problem statement
Unlock the vault to pass the level!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
Solution
Start with creating a new contract for the current level by clicking on the button, Get new instance. Remember to have enough eth in the connected wallet and that it's connected to the Sepolia network.
Open up the developer tool in your browser (F12) and in the console write this code.
const password = await web3.eth.getStorageAt(contract.address, 1)
With the password in our hands we can call the unlock function on the contract.
await contract.unlock(password)
Sign the transaction and wait for it to go through. Finish up the challenge by clicking on the button, Submit instance, to commit and update the progress on the ethernaut contract.
Explanation
It's important to know that everything is readable on the blockchain, unless you encrypt the data to obfuscate it.
The lesson here is to understand the visibility of state variables in a Solidity contract. private
only defines that other contracts can't read or modify the state variable. From the Solidity documentation we can also read the following warning:
Making something
private
orinternal
only prevents other contracts from reading or modifying the information, but it will still be visible to the whole world outside of the blockchain.
With this knowledge we only need to figure out how to read the value from the deployed contract. There is a JSON-RPC API implemented in each Ethereum node in order for applications to interact with Ethereum, more about the API can be found here. Thankfully we have access to the web3js library on the web application which enables us to use this API.
By using the function, getStorageAt
, that is defined in the API we can read the value from a storage position at a given address.
web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])
web3js has the details about the parameters needed for the function. By using the function like this.
await web3.eth.getStorageAt(contract.address, 0)
We will get the first position of the storage. Since the contract looks like this.
contract Vault {
bool public locked;
bytes32 private password;
Its not unreasonable to think that 0 points to the variable locked. We can check it by converting the hex number in the result to a number and then cast it to a boolean.
Boolean(await web3.eth.getStorageAt(contract.address, 0).then(r => web3.utils.hexToNumber(r)))
The output of that code seems correct. If we do the same exercise with the next position. This time we will convert it to a ASCII string instead, since we think it's the password state variable.
await web3.eth.getStorageAt(contract.address, 1).then(r => web3.utils.hexToAscii(r))
That gives us the string, A very strong secret password :)
, which seems to be exactly what we are looking for.
Now this code looks breakable for us.
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
We have the password so now there is no problem to fulfill the condition to set the private state variable, locked
, to false
.
await contract.unlock(password)
In the solution section we stored the password in the hex format received from the getStorageAt
call, so there is no need here to convert the variable to bytes32
which is the type expected by the function.
Last, the take away from the level author.