Data - Storage Layout
In Ethereum, all values in storage, whether they represent numbers, addresses, or array lengths, are stored as 32-byte hexadecimal values.
256-bit Address Space
In Ethereum, each smart contract has a storage space that can be thought of as a large array of 32-byte (256-bit) slots.
This means there are
2^256
possible storage slots, each capable of holding 32 bytes of data.
Storage Slots
Each storage slot is 32 bytes (256 bits).
Simple variables (e.g., uint256, address) typically occupy a single slot.
A
uint256
is 32 bytes so perfectly fills an entire slot.An
address
is 20 bytes but still takes up a whole storage slot.The remaining 12 bytes (96 bits) in the slot are padded with zeros.
A
bool
only requires 1 bit, it still occupies a full slot of 32 bytes, with 31 bytes padded with zeros.
Complex data structures (e.g., mappings, arrays) utilize multiple slots, often determined by hash functions.
Simple Variables
Simple variables like
uint256
,address
,bool
occupy one storage slot.The slot index is determined by the order of declaration in the contract.
Mappings
Hash-Based Slot Calculation.
Mappings are stored using a hash of the key and the slot number as the storage location.
For a mapping
mapping(uint256 => uint256)
, the storage slot for a keyk
iskeccak256(abi.encodePacked(k, p))
, wherep
is the slot number of the mapping itself.E.g for a mapping 5 => 99 at mapping slot 3 the storage slot of the value would be
keccak256(abi.encodePacked(5, 3))
is the slot storing 99
A mapping doesn't have an empty storage slot to show it's a mapping, it has an empty storage slot because it needs the slot number to be able to calculate where it stored the values, and there's nothing to actually store in the slot.
Arrays
Static arrays have a base slot, and elements are stored in consecutive slots starting from that base slot. Each element occupies a full 32-byte storage slot, regardless of its actual size.
Dynamic arrays store their length in the base slot, and elements are stored starting from
keccak256(baseSlot)
.
uint256[3] public fixedArray; // Base slot, e.g., slot 0, 1, 2
uint256[] public dynamicArray; // Length at slot 1, elements at keccak256(1), keccak256(1)+1, ...
Optimizing Storage with Packing
Solidity can pack multiple small variables into a single slot if they fit within the 32-byte limit and are declared consecutively. This is known as "storage packing."
contract PackedStorage {
uint128 public myUint128; // Uses the first 16 bytes of slot 0
uint128 public mySecondUint128; // Uses the next 16 bytes of slot 0
uint64 public myUint64; // Uses the first 8 bytes of slot 1
uint64 public mySecondUint64; // Uses the next 8 bytes of slot 1
uint32 public myUint32; // Uses the next 4 bytes of slot 1
uint32 public mySecondUint32; // Uses the next 4 bytes of slot 1
uint32 public myThirdUint32; // Uses the next 4 bytes of slot 1
uint32 public myFourthUint32; // Uses the next 4 bytes of slot 1
}
Storage packing can lead to significant gas savings by minimizing the number of storage slots used. However, care must be taken with the order of declaration to achieve optimal packing.
When you access a packed variable, Solidity automatically handles the correct segment of the storage slot. For example, if you access myUint64
, Solidity retrieves the bytes 0-7 from slot 1 and interprets them as a uint64
.
Empty Slots and Interpretation
Unused storage slots default to zero.
There is no inherent indicator within the slot itself to distinguish whether it is an unused slot or a slot belonging to a mapping that has not been assigned a value yet.
It is the responsibility of the compiler and the ABI to interpret storage correctly.
The contract's bytecode and ABI contain the necessary information to differentiate between different types of storage (e.g., simple variables, arrays, mappings).
Storage Example
Unexpected error with integration github-files: Integration is not installed on this space
Each storage slot is
32 bytes
long and represents the bytes version of the object
[0] 0x00...19 <-- uint256 favoriteNumber = 25; // Hex representation of 25 (0x19)
[1] 0x00...01 <-- bool someBool = true; // Hex value of 1 for true (0x1)
[2] 0x00...01 <-- uint256[] dynamicArray; // Storage slot only contains array length in hex (0x1)
[3] 0x00...0E <-- uint256[2] fixedArray = [14,15]; // Stoage of item 0 in fixedArray (0xE)
[4] 0x00...0F <-- // Stoage of item 1 in fixedArray (0xF)
[5] 0x00...00 <-- mapping(address => uint256) public balances // Empty storage slot since it's a mapping (0x0)
...
// DYNAMIC ARRAYS
// Storage locations for data in array myArray from storage slot [2]
// 2 is the slot number containing the length
// i is the index of the array item (not used for push as it just gets added to the end of the array)
[keccak256(2)] <-- myArray.push(222);
[keccak256(2) + i] <-- myArray[i];
// MAPPINGS
// Storage locations for data in mapping balances from storage slot [3]
// 3 is the slot where `balances` is stored
// 0x123... is the mapping key (an address in this example)
[keccak256(abi.encode(0x123..., 3))] <-- balances[0x123...]
Last updated