Missing Storage Gap
Detects upgradeable contracts that lack reserved storage gaps, risking storage collisions when new state variables are added in future versions.
Missing Storage Gap
Overview
Remediation Guide: How to Fix Storage Collisions
The missing storage gap detector identifies upgradeable base contracts that do not reserve storage slots for future variables. Without a gap, adding state variables to a base contract in a future upgrade shifts the storage layout of all derived contracts, corrupting their data.
This pattern is critical in the OpenZeppelin upgradeable contract model where __gap arrays reserve unused slots. Forgetting the gap in a single base contract can make the entire inheritance tree non-upgradeable without a destructive migration.
Why This Is an Issue
Solidity lays out storage variables sequentially across the inheritance chain. If BaseV1 uses slots 0-4 and Child uses slots 5-9, adding a variable to BaseV2 shifts Child’s variables from slots 5-9 to slots 6-10. The contract reads stale data from the wrong slots, causing silent corruption of balances, ownership, or access control state.
Notable incidents include the Audius governance exploit ($6M, 2022) where a storage layout mismatch between proxy versions allowed an attacker to overwrite the governance voting configuration. OpenZeppelin’s upgrade safety tooling flags this pattern, but contracts built without their framework lack this protection.
How to Resolve
Add a __gap array to every base contract in your inheritance hierarchy:
// Before: No storage gap -- cannot safely add variables in future upgrades
contract BaseV1 {
uint256 public value;
address public owner;
}
// After: Reserved storage gap allows adding up to 48 new variables
contract BaseV1 {
uint256 public value;
address public owner;
uint256[48] private __gap; // Reserve 48 slots (total = 50 per contract)
}
When adding variables in a future version, reduce the gap size accordingly:
contract BaseV2 {
uint256 public value;
address public owner;
uint256 public newVariable; // Added in V2
uint256[47] private __gap; // Reduced from 48 to 47
}
Examples
Vulnerable Code
// Base contract without gap -- adding variables here breaks derived contracts
contract GovernanceBase {
mapping(address => uint256) public votes;
uint256 public proposalCount;
// No __gap -- any new variable shifts Child's storage layout
}
contract GovernanceChild is GovernanceBase {
mapping(uint256 => Proposal) public proposals; // Starts at slot N
// If GovernanceBase adds a variable, proposals shifts to slot N+1
// corrupting all existing proposal data
}
Fixed Code
contract GovernanceBase {
mapping(address => uint256) public votes;
uint256 public proposalCount;
uint256[48] private __gap; // Reserves slots for future base variables
}
contract GovernanceChild is GovernanceBase {
mapping(uint256 => Proposal) public proposals; // Stable at slot N+50
uint256[50] private __gap; // Child also reserves its own gap
}
Sample Sigvex Output
{
"detector_id": "storage-gap-missing",
"severity": "high",
"confidence": 0.88,
"description": "Upgradeable base contract at offset 0x00 has no storage gap reservation. Adding state variables in future versions will corrupt storage layout of 3 derived contracts.",
"location": { "function": "contract-level", "offset": 0 }
}
Detection Methodology
Sigvex identifies upgradeable contracts by detecting proxy patterns (DELEGATECALL dispatch, initializer functions, EIP-1967 storage slots). For each base contract in the inheritance hierarchy, the detector:
- Reconstructs the storage layout from SLOAD/SSTORE patterns in the decompiled HIR.
- Checks for gap variables: Scans for large fixed-size array allocations at the end of each contract’s storage range.
- Validates gap arithmetic: Ensures the gap size plus used slots equals a round number (typically 50).
- Flags missing gaps: Reports any base contract in an upgradeable hierarchy that lacks a reserved gap.
Limitations
- Cannot detect gap arithmetic errors where the gap exists but the size is wrong for the number of declared variables.
- Contracts using EIP-7201 namespaced storage (structured storage) do not need gaps; these may produce false positives if the namespace pattern is not recognized.
- Diamond proxy facets using
DiamondStoragepatterns are excluded from gap analysis.
Related Detectors
- Storage Collision — detects active storage slot collisions between proxy and implementation
- Conditional Storage Collision — detects conditional storage issues
- Storage Layout Versioning — detects layout versioning issues during upgrades