Problem statement
The creator of this contract was careful enough to protect the sensitive areas of its storage.
Unlock this contract to beat the level.
Things that might help:
- Understanding how storage works
- Understanding how parameter parsing works
- Understanding how casting works
Tips:
- Remember that metamask is just a commodity. Use another tool if it is presenting problems. Advanced gameplay could involve using remix, or your own web3 provider.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Privacy {
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;
constructor(bytes32[3] memory _data) {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
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 key32 = await web3.eth.getStorageAt(contract.address, 5)
With the key32
in our hands we need to transform it to the type byte16 from byte32, since that is what the unlock method wants.
const key = key32.substring(0, 34)
await contract.unlock(key)
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. This is similar to the problem we had on Vault - Level 08, but this time we need to have more knowledge about the types and how they are stored.
From looking at the contract code we can see that the line of code that we want to break is this.
require(_key == bytes16(data[2]));
Therefore we want to know what the content of the array data
is. From before we know that we can use the function web3.eth.getStorageAt
to get the content for a variable that is declared by the contract.
From the Solidity documentation:
State variables of contracts are stored in storage in a compact way such that multiple values sometimes use the same storage slot. Except for dynamically-sized arrays and mappings (see below), data is stored contiguously item after item starting with the first state variable, which is stored in slot 0. For each variable, a size in bytes is determined according to its type. Multiple, contiguous items that need less than 32 bytes are packed into a single storage slot if possible, according to the following rules:
- The first item in a storage slot is stored lower-order aligned.
- Value types use only as many bytes as are necessary to store them.
- If a value type does not fit the remaining part of a storage slot, it is stored in the next storage slot.
- Structs and array data always start a new slot and their items are packed tightly according to these rules.
- Items following struct or array data always start a new storage slot.
If we take look at our state variables in the contract:
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;
by that list we could guess naively that the data variable will be somewhere around slot 5 without thinking about the data types and packing logic of the storage. However lets try to map the slots to each variable.
// Slot 0, first variable declared, one byte used for storage
bool public locked = true;
// Slot 1, it will need a complete slot to be stored since 256 bits / 8 = 32 bytes
uint256 public ID = block.timestamp;
// Slot 2, will take one byte of slot 2 storage
uint8 private flattening = 10;
// Slot 2, will take one more byte of slot 2 storage
uint8 private denomination = 255;
// Slot 2, still space in slot 2 and this variable will take 2 bytes of that space
uint16 private awkwardness = uint16(block.timestamp);
// Slot 3, array data always start a new slot. Each elament
// is of type bytes32 so each element gets its own slot
// data[0] <- slot 3
// data[1] <- slot 4
// data[2] <- slot 5
bytes32[3] private data;
so the guess was correct, data[2] is in slot 5
. Therefore we use following code in the web console to get the value: const key32 = await web3.eth.getStorageAt(contract.address, 5)
. Now we have the value that is used in the unlock
function.
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
However we need to convert our bytes32 value to a bytes16 value since unlock
is expecting that type for the input variable. The section Explicit Conversions from the Solidity documentation will give us the answers on how that is working.
Fixed-size bytes types behave differently during conversions. They can be thought of as sequences of individual bytes and converting to a smaller type will cut off the sequence:
bytes2 a = 0x1234; bytes1 b = bytes1(a); // b will be 0x12
That example from the docs are very similar to the one we want to do, bytes32 -> bytes16.
That's why we are doing this in the web console.
// key32 is a hex string therefore
// 16x2 = 32 characters
// +2 for the prefix 0x
const key = key32.substring(0, 34)
Now we have the key and we just need to call the unlock
function. When done we get the summary from the author.
Resources
- Privacy - This challenge
- Remix - Web based IDE for Solidity
- Solidity - Solidity documentation
- Layout of State Variables in Storage - Latest from the Solidity documentation
- web3.js - Ethereum JavaScript API documentation