β Storage
Bit Packing
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
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 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.

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.

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
When to use storage vs. memory
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) ->0x000auint24 b(3 bytes) ->0x000014address c(20 bytes) ->0x047b37ef4d76c2366f795fb557e3c15e0607b7d8address 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:
When the length of the
stringis 31 bytes or less itβs stored in a single slot starting from the left side and thelength * 2is stored in the final byte on the right.
For anything larger than 31 bytes the storage process is similar to an array. Where the slot of the
stringstoreslength * 2 + 1and the data is stored viakeccak256(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?