Ethernaut Level 12 Privacy [Foundry-Hardhat] | by 0xcsp | Coinmonks | Jan, 2023


Layout of State Variables in Storage

Making a variable private implies that it can only be accessed in the contract in which it is declared; it does not hide the variable, and the variable’s value can be read.

Nothing is private in the blockchain. We can access the contract’s storage directly using the slot number at which the variable is declared.

Each smart contract has its own storage space. Storage is a permanent data store for smart contracts. The data in storage persists across different function calls. Ethereum stores data in storage “slots”, which are 32-byte-sized slots. State variables of contracts are stored in storage in a compact way so that multiple values sometimes use the same storage slot. Except for dynamically-sized arrays and mappings, data is stored contiguously, item after item, starting with the first state variable, which is stored in slot 0. For each variable, the 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.

Declare your storage variables in the order of uint128, uint128, uint256 instead of uint128, uint256, uint128, as the former will only take up two slots of storage, whereas the latter will take up three.

If you are not reading or writing all the values in a slot at the same time, this can have the opposite effect, though.

constant and immutable variables will not take a storage slot because they are directly replaced in the code at compile time(constant) or during deployment time (immutable).

Take a look at the level contract:

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;

The unlock(bytes16 _key) function simply checks if the byte16 _key input we have passed matches the data[2] value. If true then we unlock the contract and passed the challenge.To unlock the contract we need to get the data at data[2] and downcast it from bytes32 to bytes16.

Here’s how the contract stores the data:

To get the data at data[2], we need to read slot 5.

  • We can read the contract storage using the getStorageAt() function, passing the contract address and slot number to read as arguments. instance contains the address of the deployed level contract.
const data = await web3.eth.getStorageAt(instance,5)
  • Cast data to bytes16 to get the key.

The data returned is the hex string representation of the bytes32 data, with 0x appended at the beginning. Casting data to bytes16 is the same as slicing data to get the first 16 characters(18 characters including 0x).

2 hex characters represent 1 byte; hence, get the first 34 characters using the slice() method.

const key = data.slice(0,34)
await contract.unlock(key)
  • Check value of locked. It should return true.
await contract.locked()

Another method for obtaining the key is to examine the deployment transaction data on the block explorer.

Submit the instance.

Level passed!!!😄

#Ethernaut #Level #Privacy #FoundryHardhat #0xcsp #Coinmonks #Jan