Ethernaut Lvl 12 Privacy Walkthrough: How Ethereum optimizes storage to save space and be less gassy

This is a in-depth series around Zeppelin team’s smart contract security puzzles. We learn key Solidity concepts to solve the puzzles 100% on your own.

Nicole Zhu
Coinmonks
Published in
4 min readAug 30, 2018

--

To solve this level, let’s dive deeper into how Ethereum optimises data storage. But first, make sure you know how to read storage on the blockchain, i.e. in this earlier example.

If you’re doing Ethernaut out of order, first check out how to read Ethereum private variables

How Ethereum optimizes data storage

From Solidity docs, we get this definition:

Statically-sized variables (everything except mapping and dynamically-sized array types) are laid out contiguously in storage starting from position 0. Multiple items that need less than 32 bytes are packed into a single storage slot if possible, according to the following rules

Let’s break this down

Here’s an example of inefficient storage usage. Notice that smaller size variables like boolVar and bytes4Var are not sequentially initialized, taking new slots 0 and 2 when they could have been packed together:

A more efficient storage method would be to sequentially declare the bool (1 byte size) and the bytes4 (4 bytes size) variables. The EVM then efficiently packs the two into a single storage slot.

Likewise, in the Object struct, the more efficient method is to pack the two uint8s together, taking up 1 slot. This way, all future instances of Object only take 2 slots to store, rather than 3. Storage optimization is especially important in structs, as the storage can grow rapidly:

Notice: slots index at 0 from RIGHT to LEFT. Bytes4Var is initialized after boolVar, so its stored to the left of boolVar, exactly 1 byte from the .

Exceptions:

1. constants are not stored in storage. From Ethereum documentation, that the compiler does not reserve a storage slot for constant variables. This means you won’t find the following in any storage slots:

contract A {
uint public constant number = ...; //not stored in storage
}

2. Mappings and dynamically-sized arrays do not stick to these conventions. More on this at a later level.

You are now equipped to solve this level! Keep scrolling for a detailed walkthrough:

Detailed Walkthrough

To solve this level, you must figure out what’s stored in data[2], cast it into a bytes16 variable, and submit it as the key to unlock() Privacy.sol.

Prereq: how to read blockchain storage (even private variables).

0. Notice the following variable declarations in Privacy.sol. Let’s count the storage slots these take up:

You should expect locked, flattening, and denomination, which total 5 bytes, to share only 1 storage slot (with 27 bytes of free storage to spare).

You should expect the data array to take up 3 remaining slots, with one slot per data[index].

  1. To get data[2], read the storage at slot 4:

In truffle console — network Ropsten, access your level instance, and call getStorageAt(…, 3) to get data[2].

2. Using Remix, cast the bytes32 result into a bytes16 value.

3. Using Remix, invoke unlock() with the bytes16 value to unlock this level!

Key Security Takeaways

  • In general, excessive slot usage wastes gas, especially if you declared structs that will reproduce many instances. Remember to optimize your storage to save gas!
  • Save your variables to memory if you don’t need to persist smart contract state. SSTORE <> SLOAD are very gas intensive opcodes.
  • All storage is publicly visible on the blockchain, even your privatevariables!
  • Never store passwords and private keys without hashing them first

More Levels

Get Best Software Deals Directly In Your Inbox

--

--