“When I first embarked on my journey to grasp the fundamentals of Solidity, I encountered a perplexing concept: storage. To better comprehend this concept, I likened it to a key-value data structure that houses state variables within smart contracts.
To simplify this notion, consider storage as an array. In Solidity, there are a staggering 2²⁵⁶ slots available (indexed from 0 to 2²⁵⁶-1), each with a fixed length of 32 bytes.
slot[0] = data
slot[1] = data
slot[2] = data
.
.
.
slot[n] = data
State variables are stored in accordance with their declaration order, length, and whether they are of value or dynamic type.
This article endeavors to provide a concise and beginner-friendly exploration of Solidity’s Storage Layout, with a focus on clarity rather than technical intricacies. It aims to shed light on how state variables are stored within storage slots through a series of succinct examples.
If you’d like to explore these examples hands-on (which I highly recommend), you can use Remix IDE to deploy contracts and utilize the console for contract interaction.
Storage Layout for Value Types
Let’s delve into a series of examples:
Example 1:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract StorageLayout {
uint256 num = 1; // occupies slot 0
}
In this example, the contract declares a single state variable, ‘num,’ of type uint256, which occupies slot 0 because it’s the first declared state variable.
Example 2:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract StorageLayout {
uint256 x = 1; // slot 0
uint256 y = 2; // slot 1
uint256 z = 3; // slot 2
}
Here, three variables of type uint256 are declared. ‘x’ occupies slot 0 as it’s the first declared state variable, ‘y’ goes into slot 1, and ‘z’ into slot 2.
So far, so straightforward, isn’t it?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract StorageLayout {
uint16 x = 1;
uint16 y = 2;
uint16 z = 3;
}
Example 3:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract StorageLayout {
uint16 x = 1;
uint16 y = 2;
uint16 z = 3;
}
In this instance, the contract declares three state variables of type uint16, each represented with 2 bytes. When we read the content of slot 0 using the web3.js method ‘web3.eth.getStorageAt(contractAddress, slotPosition),’ we find that the three state variables have been packed together in a single slot from right to left. This happens because uint16 variables do not fill the entire 32-byte slot; Solidity pads the data with zeroes to reach 32 bytes.
All state variables in Solidity are ABI-encoded and are automatically decoded when their values are retrieved.
For instance:
web3.eth.abi.decodeParameter("uint16", "0x0000000000000000000000000000000000000000000000000000000000000001")
This command would return 1.
Example 4:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract StorageLayout {
bool status = true;
address addr = 0xCc8188e984b4C392091043CAa73D227Ef5e0d0a7;
}
If we deploy this to the Sepolia testnet and read from storage using the same commands as before, we would obtain:
slot[0] = 0x0000000000000000000000cc8188e984b4c392091043caa73d227ef5e0d0a701
Address is a unique built-in type in Solidity with a length of 20 bytes, and booleans can be represented with one byte (0x00 for false and 0x01 for true). The first byte (in bold) from right to left represents the boolean ‘status’ with a value of 0x01. The next 20 bytes represent the address, and the remaining data is left-padded with zeroes to complete the 32 bytes.
Dynamic types have the flexibility to change in size, making it impossible to store their elements sequentially in the same manner as value types.
Arrays Consider the following contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract StorageLayout {
bool private status = true; // slot 0
uint256[] private numArray = [1, 2, 3, 4, 5]; // slot 1 holds the length of the array
address private z = 0xCc8188e984b4C392091043CAa73D227Ef5e0d0a7; // slot 2
function getSlotForArrayElement(uint256 _elementIndex) public pure returns (bytes32) {
bytes32 startingSlotForArrayElements = keccak256(abi.encode(1));
return bytes32(uint256(startingSlotForArrayElements) + _elementIndex);
}
}
In this example, if we access the content in slot 1 using ‘web3.eth.getStorageAt,’ we get:
0x0000000000000000000000000000000000000000000000000000000000000005
The array contains 5 elements, but we receive a single byte (0x05) representing the array’s length. For arrays, Solidity stores the length of the array in the slot where the array was declared, while the actual data of the array is stored in other slots.
To determine the slot where the array’s data is stored, we need to compute the keccak256 hash of the index of the array’s slot declaration, which can be expressed as:
keccak256(abi.encode(ARRAY_SLOT_DECLARATION))
For ‘numArray,’ which was declared in slot 1, the formula for calculating the slot where the data is stored is as follows:
keccak256(abi.encode(1))
This calculation results in a specific slot index. For example:
0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
Converting this hex value to decimal gives us:
80084422859880547211683076133703299733277748156566366325829078699459944778998
This slot index corresponds to the data of the first element in the array, which is 1.
Similar calculations can be applied to access other elements in the array, resulting in sequentially stored slots.
Mappings
Mappings, like arrays, do not store elements sequentially. To determine the slot in which each mapping element will be stored, use the following formula:
keccak256(abi.encode(KEY, SLOT_INDEX_DECLARATION))
This formula computes the hash of the concatenation of the key with the slot where the mapping was declared. The slot acts as a salt to prevent mapping elements from overwriting slots of other mappings.
Strings and Bytes
Strings and bytes are encoded and stored in storage in a similar fashion.
Consider the following contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageLayout {
string public name = "Natelie";
}
When we access slot 0, we get:
0x506163656c6c690000000000000000000000000000000000000000000000000e
Here, ‘0x0e’ represents the length of the string (14 bytes in decimal), and ‘0x506163656c6c69’ is the actual data, which totals 14 bytes and translates to “Pacelli.”
If the string and its length do not fit within the same slot, Solidity follows the rules applied to arrays, with the length stored in the slot declaration and the data divided into 32-byte chunks stored sequentially.
Structs
For struct state variables, each slot index corresponds to an element within the struct. If the struct elements fit within a single slot, they are packed together.
Consider the following contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageLayout {
struct Car {
string brand;
uint256 year;
uint256 price;
bool isSold;
}
Car public car = Car({brand: "Subaru", year: 2012, price: 10000, isSold: true});
}
When accessing slots, you will find that each slot corresponds to a specific element within the struct.
In the case of struct elements of dynamic type, the same rules as discussed earlier apply.
Conclusion
Understanding Solidity’s storage layout is crucial for optimizing space efficiency and potentially reducing deployment and execution gas costs. Depending on the values your state variables will hold, selecting the appropriate uintN and bytesN types and declaring them in the correct order to allow for efficient packing can result in multiple variables being accessed in a single call.
It’s important to note that, in most of the examples, state variables were declared with private visibility. However, there’s a common misconception that private variables are inaccessible or secret. Using libraries like web3.js and the ‘web3.eth.getStorageAt’ method, anyone can read any slot in storage to access data, regardless of visibility. Always exercise caution and avoid storing sensitive information in an unencrypted form, especially in a public blockchain like Ethereum.”

