Solidity layout and access of storage state variables simply explained
The Storage is like a key-value data structure that holds the state variables values of a Solidity smart contract.
Thinking of the storage as an array will help us understand it better. Each space in this storage “array” is called a slot and holds 32 bytes of data (256 bits). The maximum length of this storage “array” is 2²⁵⁶-1, so we can fit a lot of elements into it.
Each state variable declared in the smart contract will occupy a slot depending on its declaration position and its type.
For example, the following StorageLayout
contract has only 1 state variable, id
, since it’s the first one, it will be stored in slot index 0 of the storage.
Take a look at the following example:
The state variable id
is located at storage slot index 0 because it’s the first declared. count
is at slot index 1 because is the second. owner
is at slot index 2 because it’s the third declared. And so on.
We could represent it as:
We see that we are assigning the number 543
to the index 0 of the storage. All the data in the storage is saved as bytes, so, converting the decimal value 543
to hexadecimal byte format would be 021f
, left padded to 32 bytes, that’s why I’m showing:
0x000000000000000000000000000000000000000000000000000000000000021f
Which is how the 543
number would be represented in storage.
We also see that not assigning a value to a state variable is equivalent to assigning its default or zero value based on the type; for a string
type, the zero value would be represented as an empty string. For an address
type the value would be represented as the zero address 0x0000000000000000000000000000000000000000
, 40 zeros or 20 empty bytes. For a uint256
it would be a 0, or 64 zeros if represented as bytes. For a bool
type the zero value would be represented as false, which is 0x00
, one byte long for this type. And so on.
All the values in the storage are saved ABI-Encoded and when retrieving the value using its variable they are decoded automatically.
We could use web3.js
to access the storage for any contract directly, doing something like this:
For example, let’s call this contract that is deployed in the testnet Rinkeby at: 0x8Aa5C5B74F35a1cB01631bCA24D995d369670E60
Calling the getStorageAt
function with storage slot index 0 returns this:
0x000000000000000000000000000000000000000000000000000000000000021f
Notice that getting the storage values directly using web3.eth.getStorageAt
does not decode the value automatically when we access the values through their variable since based on the type of the variable Solidity will know how to decode it, but web3.eth.getStorageAt
does not know what type is supposed to be at that storage slot and cannot decode it. We would need to decode it manually.
We can use web3.eth.abi.decodeParameter("type", "data")
to decode data based on its type:
That would return 543
, as expected.
You can try out the other storage slot indices to see what you get.
You can try it out with remix, in its console:
In your MetaMask, select the testnet Rinkeby network.
Then go to https://remix.ethereum.org/ and connect your MetaMask:

Then in its console, use web3
to call the contract:

That will return the value at storage slot 0 for that contract, in bytes, abi-encoded. To decode it, in the same remix console, do the following:

And that will return 543
.
(All the contract address examples will be real contracts deployed in testnet Rinkeby, so you can try it yourself).
Awesome!
Now, we have different variable types in Solidity, that are not 32 bytes long (256 bits). So, what happens if we declare state variables which types are less than 32 bytes? Do they occupy 1 slot each? Well, depending on the declaration order of 32 bytes and less the 32 bytes state variable types, some of them can share the same slot, as long as they can fit together.
Let’s look at the following table with some variable types and their size in bytes:

We see we have many options for the variable sizes.
32 bytes types occupy one slot always. Smaller types can share a common slot. For example, if we declare 4 uint64
(8 bytes) variables one below the other, then the four of them would share a single slot (since they are 4 multiplied by 8 bytes equals 32 bytes):
And we call to get the value in storage slot index 0:
web3.eth.getStorageAt("0x9168fBa74ADA0EB1DA81b8E9AeB88b083b42eBB4", 0)
It returns:
0x04000000000000000300000000000000020000000000000001
Notice how they all fit together into a single slot, from right to left, where the first variable declared occupies the right-most bytes.
And if we call storage slot index 1:
web3.eth.getStorageAt("0x9168fBa74ADA0EB1DA81b8E9AeB88b083b42eBB4", 1)
It returns:
0x0000000000000000000000000000000000000000000000000000000000000005
Because the fifth variable declared is 32 bytes (uint256) so it occupies one slot of its own.
So, these 5 state variables only occupied 2 slots, saving some space. If we access these variables directly from the contract, then Solidity will read it from storage, extract the value of the variable that we are using and return it to us.
What if we declare the first variable as uint64
(8 bytes), the second as uint256
(32 bytes) and the third as uint64
(8 bytes)? Well, these 3 variables would use 3 slots, because of the position of theuint256
type variable that is in between the 2 uint64
, so they cannot be fitted together in a single slot. This is not space efficient, and we need to analyze our state variables declaration to order them accordingly so they occupy as less space as possible.
web3.eth.getStorageAt("0x8eDf01e48279a8b59dcCDe6D06Df8A002a2132e0", 0)web3.eth.getStorageAt("0x8eDf01e48279a8b59dcCDe6D06Df8A002a2132e0", 1)web3.eth.getStorageAt("0x8eDf01e48279a8b59dcCDe6D06Df8A002a2132e0", 2)0x00000000000000000000000000000000000000000000000000000000000000010x00000000000000000000000000000000000000000000000000000000000000020x0000000000000000000000000000000000000000000000000000000000000003
Let’s try it one more time with an address
type and a uint8
type:
The sender
is 0x6827b8f6cc60497d9bf5210d602C0EcaFDF7C405
So, calling getStorageAt
:
web3.eth.getStorageAt("0xDc4CF2283e0F2c22f96884AC03362f3A7F2150Dc", 0)
Returns:
0x00000000000000000000007b6827b8f6cc60497d9bf5210d602c0ecafdf7c405
The 7b
part is our value 123
, we see how the address
and the uint8
fit together in a single slot and there is still room for more (address type is 20 bytes, uint8 is 1 byte, so there are still 11 bytes available in that slot).
Great! So now we learned how value type state variables are stored in storage. But how are the reference type values (like mapping and arrays) stored in storage? This is a bit more involved.
Reference Type storage layout
Dynamic types are not that straightforward, because they can increase and decrease the amount of data they hold dynamically. So they cannot be stored sequentially, as the value types can.
For an array, in the slot it is declared only its length is saved, and its elements are stored somewhere else in the storage, using as the storage slot index a calculated value derived from the array declaration slot index and the index of the element you need to access.
The starting storage index of the elements of the elements of an array is calculated with the keccak256
hash of the index of the array declaration, left padded to 32 bytes (if the index of the array is 0, then padding it to 32 bytes would be 0x0000000000000000000000000000000000000000000000000000000000000000). Then we sum to the result of the the hash the index of the element we want to access. Encoding the index with abi.encode
will left-padded with zeros automatically for us.
abi.encode(0)
Will return:
0x0000000000000000000000000000000000000000000000000000000000000000
So
keccak256(0x0000000000000000000000000000000000000000000000000000000000000000)
Will return:
0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
For example, in the following contract, we declare an array uint256[] public values = [1,2,3,4,5,6,7,8];
and I calculated the starting position of its elements in storage with keccak256(abi.encode(0))
, and created a utility function getElementIndexInStorage
to sum to the keccak256(abi.encode(0))
hash the index of the element we want to access:
So, if we try:
web3.eth.getStorageAt("0x080188CFeF3D9A59B80dE6C79F8f35C6843aa41D", "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563")
It will return:
0x0000000000000000000000000000000000000000000000000000000000000001
If we sum 1 to the index 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
web3.eth.getStorageAt("0x080188CFeF3D9A59B80dE6C79F8f35C6843aa41D", "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564")
Then we get:
0x0000000000000000000000000000000000000000000000000000000000000002
And so on.
These indices are huge and look random since they are calculated withkeccak256
, which will return a 256 bit number, represented as hexadecimal, and we use that hash as the index of the elements of our array. Remember that the storage capacity is 2²⁵⁶-1 elements, so we are good and in range using keccak256
hash as slot index. This makes the probability of 2 or more different state variables sharing the same slot in storage low.
Also, notice that from the keccak256
hash of the index of the array in storage + the index of the element that we want to access, we realize that from that hash the elements of that array are in sequential order.
We can convert this hash index to decimal and use it to call getStorageAt
as well, summing 1 to it and see the array values:
web3.eth.getStorageAt("0x080188CFeF3D9A59B80dE6C79F8f35C6843aa41D", "18569430475105882587588266137607568536673111973893317399460219858819262702947")web3.eth.getStorageAt("0x080188CFeF3D9A59B80dE6C79F8f35C6843aa41D", "18569430475105882587588266137607568536673111973893317399460219858819262702948")web3.eth.getStorageAt("0x080188CFeF3D9A59B80dE6C79F8f35C6843aa41D", "18569430475105882587588266137607568536673111973893317399460219858819262702949")web3.eth.getStorageAt("0x080188CFeF3D9A59B80dE6C79F8f35C6843aa41D", "18569430475105882587588266137607568536673111973893317399460219858819262702950")0x00000000000000000000000000000000000000000000000000000000000000010x00000000000000000000000000000000000000000000000000000000000000020x00000000000000000000000000000000000000000000000000000000000000030x0000000000000000000000000000000000000000000000000000000000000004
Since array elements are stored sequentially from the hash, the same space layout applies to them. If our array was uint8[]
, then many elements of the array would fit in a single slot until 32 bytes are occupied.
For example, given the following contract with an array of uint8
:
When we try to get the element at the index of the first array element:
web3.eth.getStorageAt("0x37353BD77b6F08BEB6aE5aF8a81e65Dd83Df36bA", "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563")
We get:
0x0000000000000000000000000000000000000000000000000807060504030201
We see all the values packed into one single slot.
Great! Now let’s see how mapping
elements are stored and accessed.
Mapping elements are not stored sequentially as arrays. A mapping element slot index is calculated with the keccak256
hash of the concatenation of the key of the element we want (left-padded with 0 to 32 bytes) and the declaration slot index of the mapping.
The slot where the mapping is declared does not contain any information, just empty bytes, as opposed to the arrays slots that contain the array length. But still, this slot is reserved for the mapping to know its index and use it to calculate the slot index of its elements and prevent other mappings declared somewhere else to calculate the same slot index for a different element.
Let’s look at the following contract:
The above contract declares a mapping
as the first (and only) state variable. Then it declares a constant variable (which is not put in any slot since it’s a constant) that has the value 0, indicating the index of the balances mapping in the storage slot. And a utility getStorageLocationForKey
function to calculate the storage slot index for the element for the specified mapping key.
We can also see that the constructor is initializing the mapping with 3 keys and values.
So, if we need to know the storage index of the 0x6827b8f6cc60497d9bf5210d602C0EcaFDF7C405 then we:
- Left pad the address to 32 bytes. We know the
address
type is only 20 bytes, so we need to add 12 more bytes, which would be 24 zeros, to the left:
0000000000000000000000006827b8f6cc60497d9bf5210d602c0ecafdf7c405
Notice the address needs to be all lowercase.
2) Left pad the mapping index. We know our mapping is declared at index 0 of the storage, so, 32 empty bytes:
0000000000000000000000000000000000000000000000000000000000000000
Let’s concatenate them, the address being the first element and the index the second:
0000000000000000000000006827b8f6cc60497d9bf5210d602c0ecafdf7c4050000000000000000000000000000000000000000000000000000000000000000
And calculate the keccak256
hash for it:
keccak256("0000000000000000000000006827b8f6cc60497d9bf5210d602c0ecafdf7c4050000000000000000000000000000000000000000000000000000000000000000")
Notice we don’t use the 0x
prefix for neither of these values while calculating the keccak256
hash.
The result is:
86dfc0930cb222883cc0138873d68c1c9864fc2fe59d208c17f3484f489bef04
Now let’s use this as an index while calling web3.eth.getStorageAt
:
web3.eth.getStorageAt("0x59580A79b12Df8A1763ED2e43C35ef700d20580E", "0x86dfc0930cb222883cc0138873d68c1c9864fc2fe59d208c17f3484f489bef04")
(Remember to add the 0x
prefix to the index while calling web.eth.getStorageAt
when using this hexadecimal hash)
That will return:
0x00000000000000000000000000000000000000000000000000000000000002a6
If we convert 0x02a6
to decimal, it will be 678
. So, we successfully derived the index of the element in storage for the address 0x6827b8f6cc60497d9bf5210d602C0EcaFDF7C405.
Let’s derive the index for the value for address 0x66B0b1d2930059407DcC30F1A2305435fc37315E.
// Address0x66B0b1d2930059407DcC30F1A2305435fc37315E// Left padded with zeros and in lowercase00000000000000000000000066b0b1d2930059407dcc30f1a2305435fc37315e// Concatenated with the mapping storage slot index:00000000000000000000000066b0b1d2930059407dcc30f1a2305435fc37315e0000000000000000000000000000000000000000000000000000000000000000// Its keccak256 hash (used as the index for the value)e27e5829d8ca6ec0bfea2684584a4365495fb54a392946d691782f1c389bdbaf
Now let’s use it to call web3.eth.getStorageAt
:
web3.eth.getStorageAt("0x59580A79b12Df8A1763ED2e43C35ef700d20580E", "0xe27e5829d8ca6ec0bfea2684584a4365495fb54a392946d691782f1c389bdbaf")
It returns:
0x00000000000000000000000000000000000000000000000000000000000001f5
Converting 0x01f5
to decimal is 501, as we expected.
Great!
Now let’s calculate the difference of both indices and see how “far” these mapping values for those addresses are in storage, one minus the other:
// In hex0xe27e5829d8ca6ec0bfea2684584a4365495fb54a392946d691782f1c389bdbaf - 0x86dfc0930cb222883cc0138873d68c1c9864fc2fe59d208c17f3484f489bef04// In decimal102445934991847342074335489974073047898813300039158550507599083070890119519151 - 61005257705351047550806163798727536474361099733758312108405269887786908643076
The difference is:
In hex:5B9E9796CC184C38832A12FBE473B748B0FAB91A538C264A7984E6CCEFFFECAB// In decimal41440677286496294523529326175345511424452200305400238399193813183103210876075
Wow, they are really far from each other.
This is different than with array elements that they were just 1 position apart and by simply adding one to the first hash index we got the next element.
So, we can conclude that for mapping values there is no way to order them to save space by fitting smaller types into a single slot like with arrays.
Now, let’s suppose with have two similar mapping
which contain the same keys but different values:
We already saw that calculating the slot index for the value of the 0x6827b8f6cc60497d9bf5210d602C0EcaFDF7C405 address in balances
mapping yields:
86dfc0930cb222883cc0138873d68c1c9864fc2fe59d208c17f3484f489bef04
We would want the balances2
to produce the same hash as balances
because that would rewrite the data and put something else in there.
That’s why Solidity keeps the slot where the mapping was declared, to use its index to concatenate it with the key and produce a different hash for similar mapping.
So, calculating the slot index for the value of the 0x6827b8f6cc60497d9bf5210d602C0EcaFDF7C405 address in balances2
mapping would produce a concatenation like this:
0000000000000000000000006827b8f6cc60497d9bf5210d602c0ecafdf7c4050000000000000000000000000000000000000000000000000000000000000001
Notice the 1
at the end, which is the index of the balances2
mapping. Its keccak256
hash would be:
a0096aacec19f612936b3312e53818331d7f7249803e01c241486479df3f83fa
Way different than the slot index for the same address but for different mapping.
So, calling:
web3.eth.getStorageAt("0x6799394A2Cda29f142f3690d877ea2dB6630552b", "0xa0096aacec19f612936b3312e53818331d7f7249803e01c241486479df3f83fa")
Returns:
0x000000000000000000000000000000000000000000000000000000000000007b
And converting 0x7b
to decimal is 123
, and not 678
, as we expected.
Similar approaches apply for other dynamic types like mappings where the values are other mappings or arrays, and arrays of arrays (multi-dimensional arrays). The same principles are applied, but recursively.
Take a look at the documentation for some details: https://docs.soliditylang.org/en/v0.8.14/internals/layout_in_storage.html
Strings and bytes
Strings and bytes are a bit different. For these 2 bytes, they are encoded and treated exactly the same. So let’s only use string
for our example.
Look at the following contract:
Accessing it like this:
web3.eth.getStorageAt("0xDf5CCDEf122A8f85A7B4B4e7af6aC67aD2c86Cd0", 0)
Returns:
0x4a6572656d79000000000000000000000000000000000000000000000000000c
Notice that we have bytes to the left, followed by a bunch of zeros, then we have more bytes to the right.
For string
and bytes
type, a single slot is used to hold the data and the length of it. In this case, the string “Jeremy” has 12 bytes, which are placed on the left-most side of the slot: 4a6572656d79
, if we convert that to text, we get “Jeremy”.
And if we convert the right-most byte 0c
to decimal, we get 12
, meaning that the data is 12 bytes long. This way the EVM knows how to read the data in that slot.
But, what happens if the string
is more than 31 bytes long (31 bytes because 1 other byte is used to hold the length of the data)?
Well, the same rules as accessing an array element apply. The string is split into 32-byte long chunks and put starting in the slot index calculated by keccak256(stringDeclarationSlotIndex)
, and only the length of the string is saved in the string declaration slot index.
Having this contract:
We see that it has a lot of text, way more than 32 bytes. So, accessing it like:
web3.eth.getStorageAt("0x57916E627Ab4dCEb1553EdF57615961a9aE09a57", 0)
Returns:
0x000000000000000000000000000000000000000000000000000000000000047d
Notice that the left-most bytes are empty. And we have some data on the right-most bytes. If we convert 0x047d
to decimal we get 1149
, which is the length of the string. But where is the rest of the data? Let’s find it.
We know that since name
is the first variable declared in that contract, then the slot index of the rest of the data is at keccak256(0)
:
keccak256(0)290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563web3.eth.getStorageAt("0x57916E627Ab4dCEb1553EdF57615961a9aE09a57", "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563")0x4c6f72656d20497073756d2069732073696d706c792064756d6d792074657874
If we convert those bytes to string, we get:

Great! Where is the rest of the data? Simple, as in the array examples, we simply have to sum 1 to that hash to get the next slot containing more data, and keep adding one until we get all the data:
290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 + 1290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564web3.eth.getStorageAt("0x57916E627Ab4dCEb1553EdF57615961a9aE09a57", "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564")0x206f6620746865207072696e74696e6720616e64207479706573657474696e67
We add those bytes to our conversion and:

Awesome!
We simply have to keep adding 1 and calling again to get all the data. Remember that exactly the same applies to bytes
type.
Structs
For structs state variables, the slot index where it is declared is reserved for the first value it has, the next slot for the second, the next slot for the third, and so on.
The following contract declares a struct Person
type, which does not occupy any slot in storage because that is just a declaration, a blue print for Person
struct instances.
Then, it declares a Person public p = Person(1, "Jeremy", 28, true);
struct instance, which is the first state variable declared in this contract, so its data is located starting from storage slot index 0.
The first member of this Person
instance p
, id
, is located in slot index 0. The second member, name
, is located in slot index 1, age
in slot index 2 and isActive
is slot index 3.
So, if we call:
web3.eth.getStorageAt("0xA9Edf9cEbEA00095747731324bf0db65d3CC7B20", 0)web3.eth.getStorageAt("0xA9Edf9cEbEA00095747731324bf0db65d3CC7B20", 1)web3.eth.getStorageAt("0xA9Edf9cEbEA00095747731324bf0db65d3CC7B20", 2)web3.eth.getStorageAt("0xA9Edf9cEbEA00095747731324bf0db65d3CC7B20", 3)
We get:
0x00000000000000000000000000000000000000000000000000000000000000010x4a6572656d79000000000000000000000000000000000000000000000000000c0x000000000000000000000000000000000000000000000000000000000000001c0x0000000000000000000000000000000000000000000000000000000000000001
As expected.
If the struct contains members of mapping
and dynamic arrays, then the previous layout rules discussed above apply.
Also, the rule about fitting multiple state variable values together in a single slot if their types are less than 32 bytes long, also applies:
And we call:
web3.eth.getStorageAt("0x3C9ea065b349B1AaEEF5f49712dEfC47D237dB51", 0)
We get all the values fit into a single slot, the slot index 0:
0x0000000000000004000000000000000300000000000000020000000000000001
Conclusion
Knowing how state variables are layout in storage is important for space efficiency and to be able to access any value directly, whether it’s in the storage as is, in a mapping, or in an array, without really needing to call the contract.
There is no such thing as private data in a smart contract and blockchain. Even if some state variable is declared private
in a contract and there is no function to return those values to us, we can still use something like web3.eth.getStorageAt
to get any data from any smart contract.
Resources
Calculate the keccak256
hash online (remember to change the Input type to “Hex”):
Convert hex to decimal and vice versa online:
Convert bytes to text:
https://onlinestringtools.com/convert-bytes-to-string
Remember to follow me for more content like this.
Leave a comment if you found this useful or have any thoughts or corrections.