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.
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.
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 uint8
s 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]
.
- 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
private
variables! - Never store passwords and private keys without hashing them first