Skip to content

Instantly share code, notes, and snippets.

@jaybuidl
Last active September 4, 2025 23:25
Show Gist options
  • Save jaybuidl/ee377f9d21dda2d458fb669ce41908de to your computer and use it in GitHub Desktop.
Save jaybuidl/ee377f9d21dda2d458fb669ce41908de to your computer and use it in GitHub Desktop.
Storage gap arrays usefulness in struct arrays

Storage Layout in Solidity

First, let's understand how Solidity stores data:

  • Storage slots are 32-byte (256-bit) containers.
  • Each storage variable gets assigned to slots sequentially.

The Problem: Storage Collision in Upgrades

Let me illustrate with a visual example:

Initial Contract (V1)

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
...

Upgraded Contract WITHOUT Storage Gap (V2) - DANGEROUS! ❌

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

The Solution: Storage Gaps

Proper Implementation with Storage Gap

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)            │
└─────────────────────────────────────┘

Safe Upgrade with Storage Gap (V2) ✅

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])
└──────────────┘            └──────────────┘

Key Benefits Visualized

1. Consistent Struct Size

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

2. Safe Addition of Fields

Gap Usage Timeline:
V1: [id][owner][←── 10 empty slots ──→]
V2: [id][owner][value][←── 9 empty ──→]
V3: [id][owner][value][active][←── 8 ──→]

3. Array Operations Stay Valid

// 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);
}

Best Practices

  1. Size your gaps appropriately:

    uint256[50] __gap;  // For contracts likely to evolve
    uint256[10] __gap;  // For stable structs
  2. 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
    }
  3. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment