βœ…Storage

Bit Packing

Layout of State Variables in Storage

State variables of contracts are stored in storage in a compact way such that multiple values may use the same storage slot (except for dynamically-sized arrays and mappings). This is known as "Bit Packing". If the previous state variable is less than 32 bytes and the next state variable can fit into the same 32-byte slot, then they will be grouped into the same slot. Otherwise, a new slot is going to be used.

The elements of structs and arrays are stored after each other, just as if they were given as individual values.

For an example, take a look at the contract from Ethernaut Privacy:

Analysis of the storage:

State Variables in Inheritance

For contracts that use inheritance, the ordering of state variables is determined by the C3-linearized order of contracts starting with the most base-ward contract. If allowed by the above rules, state variables from different contracts do share the same storage slot.

Dynamically-Sized Types

Understanding Ethereum Smart Contract Storage

Using reserved slots works well for fixed-size state variables, but it doesn’t work for dynamically-sized arrays and mappings because there’s no way of knowing how many slots to reserve.

If you’re thinking of computer RAM or hard drive as an analogy, you might expect that there’s an β€œallocation” step to find free space to use and then a β€œrelease” step to put that space back into the pool of available storage.

This is unnecessary due to the astronomical scale of smart contract storage. There are 22562^{256} locations to choose from in storage, which is approximately the number of atoms in the known, observable universe. You could choose storage locations at random without ever experiencing a collision. The locations you chose would be so far apart that you could store as much data as you wanted at each location without running into the next one.

Of course, choosing locations at random wouldn’t be very helpful, because you would have no way to find the data again. Solidity instead uses a hash function to uniformly and repeatably compute locations for dynamically-sized values.

Dynamically-Sized Arrays

A dynamically-sized array needs a place to store its size as well as its elements.

In the above code, the dynamically-sized array d is at slot 5, but the only thing that’s stored there is the size of d. The values in the array are stored consecutively starting at the hash of the slot.

Dynamically-Sized Arrays

The following Solidity function computes the location of an element of a dynamically-sized array:

Mappings

A mapping requires an efficient way to find the location corresponding to a given key. Hashing the key is a good start, but care must be taken to make sure different mappings generate different locations.

In the above code, the β€œlocation” for e is slot 6, and the location for f is slot 7, but nothing is actually stored at those locations. (There’s no length to be stored, and individual values need to be located elsewhere.)

To find the location of a specific value within a mapping, the key and the mapping’s slot are hashed together.

Mappings

The following Solidity function computes the location of a value:

Note that when keccak256 is called with multiple parameters, the parameters are concatenated together before hashing. Because the slot and key are both inputs to the hash function, there aren’t collisions between different mappings.

Combinations of Complex Types

Dynamically-sized arrays and mappings can be nested within each other recursively. When that happens, the location of a value is found by recursively applying the calculations defined above. This sounds more complex than it is.

To find items within these complex types, we can use the functions defined above. To find g[123][0]:

To find h[2][456]:

Storage Deep Dive

A Low-Level Guide To Solidity's Storage Management

When to use storage vs. memory

When we first load a storage slot it's cold, meaning it’s more expensive at 2100 gas and whenever we call that newly used storage slot again it’s a warm storage slot, meaning it’ll be 100 gas but not as cheap as memory which is at least 3 gas (MLOAD); but can go higher if memory expansion occurs!

For an example, here is an unoptimised contract:

To optimize s.b load, we can store a copy of s.b in memory. This will cost 1 SLOAD and future operations will be MLOADs, therefore the total cost is cheaper than multiple SLOADs. The optimized contract:

Manually assigning storage

Let's play with inline assembly in order to manipulate storage. Here is a contract that uses struct:

Access uint256 boring at storage slot 0:

Lets step it up a notch with a bitpacked struct! Bitpacked means storing multiple variables in a single slot (32 bytes) by ordering the byte size of the variables in a way that results the slot being equal or less to 32 bytes. In this case we pack a total of 25 bytes into a single slot at 0x01 using:

  • uint16 a (2 bytes) -> 0x000a

  • uint24 b (3 bytes) -> 0x000014

  • address c (20 bytes) -> 0x047b37ef4d76c2366f795fb557e3c15e0607b7d8

  • address d (20 bytes) -> 0x047b37ef4d76c2366f795fb557e3c15e0607b7d8

s_struct's slots would be:

Notice how a, b, c are in the same slot. The way we grab any of these values by shifting the bits and using masking to grab a specific string of bits in a slot.

Lets go through a practical example of grabbing s_struct.b:

But what if we wanted to change the value of s_struct.b? A little complicated:

Next is dynamic array. Accessing s_array[0].d:

Next is mapping. Accessing s_map[2].b:

Accessing s_map[4].d:

string and bytes have identical encoding types that are very annoying to deal with:

  1. When the length of the string is 31 bytes or less it’s stored in a single slot starting from the left side and the length * 2 is stored in the final byte on the right.

  1. For anything larger than 31 bytes the storage process is similar to an array. Where the slot of the string stores length * 2 + 1 and the data is stored via keccak256(slot) + i

This way you can see what type of string it is from checking if lowest bit is set (the far right byte).

Last updated

Was this helpful?