# Upgradeable Solidity without storage gaps: understanding ERC-7201 and OpenZeppelin's namespaced storage

If you have worked with upgradeable Solidity contracts for a while, you probably have a slightly unhealthy fear of state variables.

Adding a function? Fine.

Changing some internal logic? Usually fine.

Adding one innocent-looking variable to the wrong contract?

That can destroy your storage layout.

I have always found this to be one of the stranger parts of upgradeable contract development. The proxy itself is not that difficult to understand. `delegatecall` runs implementation code while keeping the proxy's storage.

The dangerous part is what happens months later when V2, V3 and V4 start touching that same storage.

For years, one of the common answers was storage gaps.

OpenZeppelin 5 has moved to something much cleaner for its upgradeable contracts: ERC-7201 namespaced storage. And with Solidity 0.8.35, the Solidity compiler now understands the ERC-7201 storage-slot formula through a native `erc7201()` builtin.

I think this is worth understanding even if you normally let OpenZeppelin handle upgrades for you, because it changes how we should think about contract storage.

## The actual problem is storage

Start with a normal Solidity contract:

```solidity
contract Vault {
    address public owner;
    uint256 public totalDeposits;
}
```

Solidity assigns storage sequentially.

Roughly:

```text
slot 0 -> owner
slot 1 -> totalDeposits
```

Simple enough.

Now imagine `Vault` is running behind a proxy.

The proxy owns the storage. The implementation contains the logic.

```text
              calls
User --------------------> Proxy
                            |
                            | delegatecall
                            v
                       Implementation

Storage stays inside Proxy
```

Suppose version one has this:

```solidity
contract VaultV1 {
    address public owner;
    uint256 public totalDeposits;
}
```

And later somebody deploys this as V2:

```solidity
contract VaultV2 {
    address public treasury;
    address public owner;
    uint256 public totalDeposits;
}
```

We have a problem.

`owner` used to live in slot 0.

Now `treasury` expects slot 0 and `owner` expects slot 1.

The implementation changed, but the proxy's existing storage did not magically reorganize itself.

What V2 thinks is `treasury` may actually be the old owner.

What V2 thinks is `owner` may be part of the old `totalDeposits`.

That is why upgradeable storage rules are so strict. Existing variables generally cannot just be reordered, removed or have their types changed. New variables normally have to be appended in a storage-compatible way.

Inheritance makes this even more annoying.

## The old storage gap approach

If you have looked through older upgradeable contracts, you have probably seen something like this:

```solidity
uint256[50] private __gap;
```

It looks weird the first time you see it.

The idea is pretty clever though.

A base contract reserves a chunk of storage for future variables.

Imagine:

```solidity
abstract contract RewardsModule {
    uint256 public rewardRate;

    uint256[49] private __gap;
}
```

Later, the contract needs another variable.

```solidity
abstract contract RewardsModule {
    uint256 public rewardRate;
    address public rewardToken;

    uint256[48] private __gap;
}
```

Instead of pushing everything after the contract into new storage locations, we consume one of the slots we deliberately reserved.

OpenZeppelin's upgrade documentation still explains this pattern and its upgrade tooling can validate whether the gap has been adjusted correctly.

Storage gaps work.

But they are also bookkeeping.

You have to think about how many slots were reserved, how packing affects the slot count, which parent contract owns which variables and how changes to inheritance affect everything downstream.

On a small contract, this might not bother you.

On a protocol with several layers of inherited contracts, it gets harder to reason about.

That is where namespaced storage starts making a lot more sense.

## ERC-7201 changes the mental model

ERC-7201 takes a different approach.

Instead of treating contract storage as one long sequence owned by the entire inheritance tree, related variables can be grouped into a struct.

That struct gets its own namespace.

For example:

```solidity
/// @custom:storage-location erc7201:myapp.storage.Rewards
struct RewardsStorage {
    uint256 rewardRate;
    address rewardToken;
    mapping(address => uint256) pendingRewards;
}
```

`myapp.storage.Rewards` identifies this particular storage namespace.

Instead of assuming this struct begins at slot 0, ERC-7201 derives a pseudorandom storage location from that namespace.

The standard defines the location as:

```text
keccak256(
    keccak256(namespace) - 1
) & ~0xff
```

More precisely, ERC-7201 standardizes the formula and the `@custom:storage-location erc7201:<NAMESPACE_ID>` annotation so tools can understand what the developer intends.

Now our storage starts looking less like this:

```text
Contract storage

slot 0
slot 1
slot 2
slot 3
slot 4
...
```

and more like this:

```text
                     Proxy storage
                          |
          +---------------+---------------+
          |               |               |
          v               v               v

   Rewards namespace   Access namespace   Vault namespace
          |               |               |
     rewardRate          roles          deposits
     rewardToken         admins         balances
     pendingRewards
```

Each module gets its own part of the storage space.

That separation is the interesting part.

## Why not just use `keccak256("something")`?

You might reasonably ask why we need an ERC for this.

Developers have been using hashed storage locations for years.

Diamond storage patterns already did something similar.

The problem is that everybody inventing their own version makes tooling harder and leaves more room for mistakes.

ERC-7201 gives tools and developers a common convention.

A namespace is represented by a struct, and the struct can be annotated:

```solidity
/// @custom:storage-location erc7201:myapp.storage.Vault
struct VaultStorage {
    uint256 totalDeposits;
}
```

The namespace ID should be unique within the contract and its inherited contracts. OpenZeppelin uses IDs such as `openzeppelin.storage.ERC20` and `openzeppelin.storage.Ownable` for its own modules.

Your application could follow a similar convention:

```text
myprotocol.storage.Vault
myprotocol.storage.Rewards
myprotocol.storage.Staking
myprotocol.storage.Access
```

The exact naming scheme matters less than consistency and uniqueness.

## What OpenZeppelin 5 changed

This is where ERC-7201 becomes more than an interesting EIP.

OpenZeppelin adopted namespaced storage in the upgradeable variant of OpenZeppelin Contracts starting with version 5.0.

If you browse modern OpenZeppelin upgradeable contracts, you will see patterns that look roughly like this:

```solidity
/// @custom:storage-location erc7201:openzeppelin.storage.SomeModule
struct SomeModuleStorage {
    uint256 value;
    address account;
}
```

and an internal function that returns a storage pointer:

```solidity
function _getSomeModuleStorage()
    private
    pure
    returns (SomeModuleStorage storage $)
{
    assembly {
        $.slot := SOME_MODULE_STORAGE_LOCATION
    }
}
```

Then instead of directly accessing:

```solidity
value = 10;
```

the implementation retrieves its namespace:

```solidity
SomeModuleStorage storage $ = _getSomeModuleStorage();

$.value = 10;
```

That `$` variable is not magic Solidity syntax. It is just a variable name OpenZeppelin uses for the storage pointer.

The interesting bit is the slot assigned inside assembly.

Each module knows where its own storage begins.

## This fixes a nasty inheritance problem

Imagine this inheritance structure:

```text
        AccessControl
             |
          Rewards
             |
            App
```

With traditional sequential storage, parent variables contribute to the storage layout of the final contract.

Changing inheritance can therefore change where variables land.

With properly separated ERC-7201 namespaces, `Rewards` can own:

```text
myapp.storage.Rewards
```

while access control owns another namespace.

Their storage no longer depends on both contracts lining up nicely in one shared sequential layout.

OpenZeppelin says its namespaced storage approach allows new variables to be added to modules without compromising existing deployments. It can also allow inheritance order changes without affecting the resulting storage layout, provided all inherited contracts involved use namespaced storage.

That last condition matters.

ERC-7201 does not magically make every inheritance change safe.

## What Solidity 0.8.35 added

There is an interesting timeline here.

ERC-7201 was created in 2023.

OpenZeppelin Contracts Upgradeable adopted the pattern starting with version 5.0.

But developers still had to calculate the ERC-7201 storage location themselves and normally store the resulting hash as a constant.

Then Solidity 0.8.35 arrived in April 2026.

It added:

```solidity
erc7201(string memory id)
```

The compiler calculates the ERC-7201 base storage slot at compile time. Solidity describes this as its first "comptime builtin."

That means something like this becomes possible:

```solidity
uint256 private constant REWARDS_STORAGE_LOCATION =
    erc7201("myapp.storage.Rewards");
```

rather than manually calculating and pasting a large hexadecimal value.

The function executes during compilation, so this is not a runtime storage calculation that users pay gas for.

Solidity also allows the builtin inside its newer custom storage layout syntax:

```solidity
contract Rewards
    layout at erc7201("myapp.storage.Rewards")
{
    uint256 rewardRate;
    address rewardToken;
}
```

The `layout at` feature lets a contract choose a custom base slot for its state instead of starting from the normal slot zero. Solidity's documentation explicitly allows `erc7201()` as that compile-time base-slot expression.

There is an important distinction here.

`layout at erc7201(...)` and OpenZeppelin's namespaced struct pattern are related ideas, but they are not the same API.

OpenZeppelin's upgradeable contracts organize individual modules around ERC-7201 storage structs. Solidity's `layout at` shifts the storage layout of a contract and its inherited state to another base location.

I would not blindly replace one pattern with the other.

## What an application module can look like

A simplified namespaced module using modern Solidity could look conceptually like this:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.35;

abstract contract RewardsStorageModule {
    /// @custom:storage-location erc7201:myapp.storage.Rewards
    struct RewardsStorage {
        uint256 rewardRate;
        address rewardToken;
        mapping(address => uint256) pendingRewards;
    }

    uint256 private constant REWARDS_STORAGE_LOCATION =
        erc7201("myapp.storage.Rewards");

    function _getRewardsStorage()
        internal
        pure
        returns (RewardsStorage storage $)
    {
        assembly {
            $.slot := REWARDS_STORAGE_LOCATION
        }
    }
}
```

Your actual contract can then use the module:

```solidity
contract Rewards is RewardsStorageModule {
    function setRewardRate(uint256 newRate) external {
        RewardsStorage storage $ = _getRewardsStorage();
        $.rewardRate = newRate;
    }

    function rewardRate() external view returns (uint256) {
        RewardsStorage storage $ = _getRewardsStorage();
        return $.rewardRate;
    }
}
```

Later, V2 might need another variable:

```solidity
struct RewardsStorage {
    uint256 rewardRate;
    address rewardToken;
    mapping(address => uint256) pendingRewards;

    uint256 totalDistributed;
}
```

We append it inside the same namespace.

Other modules do not need to move.

## ERC-7201 does not mean "storage safety solved"

This is probably the most important part of the article.

Namespaces isolate storage.

They do not give us permission to randomly mutate the contents of a namespace.

This is still dangerous:

```solidity
// V1

struct RewardsStorage {
    uint256 rewardRate;
    address rewardToken;
}
```

Then:

```solidity
// BAD V2

struct RewardsStorage {
    address rewardToken;
    uint256 rewardRate;
}
```

You reordered existing state.

The namespace still points to the exact same storage location.

So the old data is still sitting there in its original layout while your new implementation interprets it differently.

Namespaced storage fixes one class of upgrade problems. It does not remove normal storage compatibility rules inside each namespace.

OpenZeppelin's Upgrades plugins understand ERC-7201 annotations and validate changes inside namespaces using their normal storage upgrade safety rules. Solidity 0.8.20 or newer is required for that validation because earlier compiler output does not contain enough information about these annotations.

This is why I would still use the OpenZeppelin upgrade validation tooling rather than treating ERC-7201 as an excuse to become adventurous with storage.

## There is another easy mistake

Look closely at this:

```solidity
/// @custom:storage-location erc7201:myapp.storage.Rewards
```

and this:

```solidity
erc7201("myapp.storage.Rewards")
```

Those strings need to describe the same namespace.

The annotation documents the namespace for tooling.

The actual slot calculation controls where your contract reads and writes.

ERC-7201's specification notes that the annotation itself does not force the compiler to use that location. Developers are still responsible for implementing the layout correctly.

So this would be a terrible typo:

```solidity
/// @custom:storage-location erc7201:myapp.storage.Rewards

uint256 private constant STORAGE_LOCATION =
    erc7201("myapp.storage.Reward");
```

One missing `s`.

Now your annotation describes one namespace while the contract accesses another.

The new Solidity builtin removes the need to manually reproduce the hashing formula. It does not remove the need to review your namespace strings.

## Should we stop using `__gap`?

For new modular upgradeable systems, I would strongly consider ERC-7201 namespaced storage.

The code is easier for me to reason about because storage ownership becomes explicit.

The rewards module owns rewards storage.

The staking module owns staking storage.

The permissions module owns permissions storage.

I do not have to mentally flatten an entire inheritance tree before understanding whether adding a variable is safe.

That said, I would not turn "storage gaps are old" into "delete every `__gap` from your existing system."

Existing upgradeable deployments have an existing layout.

Their history matters.

Migrating a live OpenZeppelin 4.x system to 5.x deserves particular care. OpenZeppelin explicitly says storage layouts should not be assumed compatible across major versions, and gives upgrading from 4.9.3 to 5.0.0 as an example of something that is unsafe to assume.

ERC-7201 is a better architecture for many new contracts.

It is not a time machine.

## Why I like this pattern

Most improvements in Solidity are obvious when you read the code.

Custom errors save gas.

Transient storage gives us transaction-scoped state.

New opcodes give us new capabilities.

ERC-7201 is different. The improvement is mostly organizational.

And that is exactly why I think it matters.

Upgradeable contracts tend to become difficult after they have lived for a few years. There are multiple implementations, several inherited modules, new developers touching the code and real money sitting behind a proxy that cannot afford a storage mistake.

At that point, having storage explicitly separated into:

```text
protocol.storage.Vault
protocol.storage.Rewards
protocol.storage.Staking
protocol.storage.Access
```

is easier to reason about than:

```text
slots 0 to 8 belong to this contract
slots 9 to 57 are reserved
then a parent uses...
wait, did this uint128 pack with the address?
```

The EVM is still just reading and writing 256-bit storage slots.

ERC-7201 does not change that.

What changed is how we organize those slots and how our tools understand them.

Sometimes the boring changes are the ones that save you from the nastiest bugs.

## One last thing

If I were starting a new upgradeable Solidity project today, my checklist would look roughly like this:

Use `@openzeppelin/contracts-upgradeable` rather than the normal package for contracts designed to sit behind proxies.

Keep state grouped into clearly named ERC-7201 namespaces.

Never reuse namespace IDs across inherited modules.

Append new variables rather than reordering existing fields inside a namespace.

Run OpenZeppelin's upgrade validation before every implementation upgrade.

And if the codebase can use Solidity 0.8.35 or newer, use the compiler's native `erc7201()` calculation instead of manually recreating the formula.

The proxy pattern did not suddenly become safe.

We just have a much better way to organize the part that was easiest to break.
