First, let's understand how Solidity stores data:
- Storage slots are 32-byte (256-bit) containers.
- Each storage variable gets assigned to slots sequentially.
Let me illustrate with a visual example:
contract StorageV1 {
struct Object {
uint256 id; // slot calculation: array_slot + (index * 2) + 0
address owner; // slot calculation: array_slot + (index * 2) + 1
}
Object[] public objects; // slot 0 (stores length)
// actual data starts at keccak256(0)
}
Storage Layout V1:
Slot 0: objects.length = 2
Slot keccak256(0) + 0: objects[0].id
Slot keccak256(0) + 1: objects[0].owner
Slot keccak256(0) + 2: objects[1].id
Slot keccak256(0) + 3: objects[1].owner
...
contract StorageV2 {
struct Object {
uint256 id;
address owner;
uint256 value; // NEW FIELD! This breaks storage layout
}
Object[] public objects;
}
Storage Layout V2 (Broken):
Slot 0: objects.length = 2
Slot keccak256(0) + 0: objects[0].id
Slot keccak256(0) + 1: objects[0].owner
Slot keccak256(0) + 2: objects[0].value ❌ COLLISION! Was objects[1].id
Slot keccak256(0) + 3: objects[1].id ❌ COLLISION! Was objects[1].owner
Slot keccak256(0) + 4: objects[1].owner ❌ New slot
Slot keccak256(0) + 5: objects[1].value ❌ New slot
contract StorageV1 {
struct Object {
uint256 id;
address owner;
uint256[10] __gap; // Reserve 10 slots per struct
}
Object[] public objects;
}
Visual Storage Layout with Gap:
┌─────────────────────────────────────┐
│ Slot 0: objects.length │
├─────────────────────────────────────┤
│ objects[0] starts at keccak256(0): │
├─────────────────────────────────────┤
│ +0: id │
│ +1: owner │
│ +2: __gap[0] (reserved) │
│ +3: __gap[1] (reserved) │
│ ... │
│ +11: __gap[9] (reserved) │
├─────────────────────────────────────┤
│ objects[1] starts at +12: │
├─────────────────────────────────────┤
│ +12: id │
│ +13: owner │
│ +14: __gap[0] (reserved) │
│ ... │
│ +23: __gap[9] (reserved) │
└─────────────────────────────────────┘
contract StorageV2 {
struct Object {
uint256 id;
address owner;
uint256 value; // NEW: Takes from gap
bool active; // NEW: Takes from gap
uint256[8] __gap; // Reduced gap size (10 - 2 = 8)
}
Object[] public objects;
}
Visual Comparison:
Before Upgrade: After Upgrade:
┌──────────────┐ ┌──────────────┐
│ id │ │ id │ (same slot)
│ owner │ │ owner │ (same slot)
│ __gap[0] │ ────> │ value │ (was gap[0])
│ __gap[1] │ ────> │ active │ (was gap[1])
│ __gap[2] │ │ __gap[0] │ (was gap[2])
│ ... │ │ ... │
│ __gap[9] │ │ __gap[7] │ (was gap[9])
└──────────────┘ └──────────────┘
Each Object ALWAYS occupies 12 slots (2 + 10 gap)
This ensures array indexing remains correct:
- objects[0]: slots 0-11
- objects[1]: slots 12-23
- objects[2]: slots 24-35
Gap Usage Timeline:
V1: [id][owner][←── 10 empty slots ──→]
V2: [id][owner][value][←── 9 empty ──→]
V3: [id][owner][value][active][←── 8 ──→]
// This calculation remains constant across upgrades:
function getObjectStorageSlot(uint256 index) pure returns (uint256) {
uint256 arrayDataSlot = uint256(keccak256(abi.encode(0)));
uint256 structSize = 12; // Always 12 slots per struct
return arrayDataSlot + (index * structSize);
}
-
Size your gaps appropriately:
uint256[50] __gap; // For contracts likely to evolve uint256[10] __gap; // For stable structs
-
Document gap usage:
struct Object { uint256 id; address owner; // v2: added value (consumes __gap[0]) uint256 value; // v3: added active (consumes __gap[1]) bool active; uint256[8] __gap; // Originally 10, consumed 2 }
-
Consider packed structs:
struct Object { uint256 id; address owner; // 20 bytes uint96 smallValue; // 12 bytes (packed with owner) uint256[9] __gap; // Less gap needed due to packing }
The storage gap pattern is crucial for maintaining storage layout compatibility in upgradeable contracts, especially with arrays of structs where each element must maintain consistent sizing across upgrades.