DeFi Security AllianceRequest an audit
Menu

EVM patterns

ERC7201 Storage Security: Namespaces, Layout and Upgrade Evidence

An isolated storage namespace does not make changes inside its structure compatible. ERC7201 storage security requires verifying the declared root, actual access path and the meaning of populated state after an upgrade.

Three white cabinets on separate blue plinths with open drawers, illustrating namespace isolation and internal storage layout.

Key facts

Complete package census
58 unique namespace IDs across all 84 Solidity files in v5.4.0
Earlier package
38 IDs across all 57 Solidity files in v5.0.0
Version difference
20 added, 38 retained and 0 removed identifiers
Integrity
Both downloaded tarballs match published SHA512 integrity
Limit
Lexical source census, no compiler validation or executed upgrade

Separate namespace isolation from upgrade safety

A namespace can remain isolated while its fields become incompatible with an earlier implementation.

ERC7201 storage security requires both questions to be answered: where does the structure begin, and what does each stored value mean after an upgrade? A namespace annotation answers neither question by itself. The implementation must use the declared location correctly, and the release must preserve the intended layout within it.

A storage namespace is an ordered collection of variables rooted at a chosen storage location. Arrays and mappings can belong to that collection. ERC7201 documents namespaces through @custom:storage-location annotations and defines a root convention that separates their storage trees from ordinary layout and other namespaces under its stated assumptions.

The root formula combines an initial hash, an offset, another hash and a mask. Its disjointness rationale assumes Keccak collision resistance and reasonably sized arrays. This is a structural argument about storage locations, not a guarantee that a new implementation reads the right field or that an upgrade preserved application balances. Keep that distinction visible in the release decision.

Distinct storage questions that an upgrade review must answer
QuestionRelevant artifactInsufficient shortcut
Where is the namespace rooted?Identifier, formula and actual access pathAnnotation text alone
Are field locations compatible?Compiler layout and upgrade validationUnchanged namespace identifier
Does stored state keep its meaning?Populated-state rehearsal and invariantsSuccessful deployment
Can initialization alter old state?Exact upgrade payload and initializer traceLibrary version label

OpenZeppelin's upgradeable library uses this convention starting with its fifth major version. That establishes a useful source population to inspect. It does not imply that a deployed application can migrate from an earlier storage design by changing imports. Existing state remains at its existing locations until a reviewed transformation actually changes it.

The ERC7201 specification explains the namespace convention. Our upgradeable contract audit guide covers the wider release process. This article narrows the job to inventorying namespace identity, checking access and preserving field meaning.

The useful review unit is the deployed inheritance and storage graph. A package can contain many well-formed namespace declarations while an application uses only a subset. Conversely, custom application code can introduce storage access absent from the package. A complete library census provides evidence about the library, then the application review must connect that evidence to the code actually deployed.

Original census of two complete library packages

Method

We downloaded the complete published @openzeppelin/contracts-upgradeable packages for 5.0.0 and 5.4.0, verified their published integrity values and inspected every Solidity file. The selection rule was the package contents, not a handpicked list of familiar contracts. A lexical census recorded each ERC7201 annotation, its identifier, associated structure and observed root literal.

Population
All .sol members in each complete npm package tarball.
Retrieval
Package metadata and tarballs fetched on ; both tarballs matched their published SHA512 integrity values.
Inclusion rule
Every Solidity member remained in the file denominator. Annotation matches formed the namespace denominator.
Missing data and exclusions
The earlier tarball has 128 regular members, of which 71 non-Solidity files were excluded from the Solidity denominator. The later tarball has 170 regular members, excluding 86 non-Solidity files. Neither tarball contains a non-regular member. No manually selected contract was substituted.
Artifacts
seo/research/integration-boundaries-2026-10-11/support-storage-timelock/source-and-measurements.py and erc7201-package-census.json; complete per-file rows and raw tarballs remain in the repository, not public downloads.

Results

Complete package census retrieved
Package versionSolidity filesAnnotation-bearing filesUnique namespace IDs
5.0.0573838
5.4.0845858

OpenZeppelin's later package retained all 38 earlier namespace identifiers, added 20 and removed none. Each observed annotation had one associated literal root candidate. For the retained identifiers, the literal root constants and normalized structure declaration lists were unchanged in this lexical comparison. These are source observations, not a compiler's compatibility verdict.

The added identifiers include storage associated with keyed nonces, additional signing schemes and governance extensions. Their presence means the package offers additional annotated structures. It does not mean an existing application gained those features, added those namespaces to its deployed graph or initialized their state correctly.

Limits

  • The census did not compile Solidity or execute an upgrade. It does not establish deployed compatibility.
  • Literal roots were recorded and their low-byte alignment observed. The script did not independently recompute Ethereum Keccak roots or prove collision freedom.
  • Normalized declarations omit application meaning. Equal fields can have changed semantics, while changed declarations require further analysis before any incompatibility conclusion.

Our reproducible result is an inventory baseline: 58 distinct annotated identifiers across 84 Solidity files in the pinned later package. The acceptance task is to compare the application's actual storage graph and release artifacts against that inventory, rather than promote the count into a security score.

Preserve meaning inside each namespace

A namespace identifier is part of storage identity. Renaming it is different from renaming a field at the same location. A new identifier can select a new root while leaving old values untouched. A field rename can keep reading the old stored value. A release reviewer needs to inspect the resulting locations, not infer behavior from a more descriptive source name.

Within the namespace, ordinary layout rules still matter. Reordering existing fields or changing their types can change how the same words are decoded. Packed values make that boundary less obvious to a casual source review because multiple fields can share a slot. Mapping and array members also have derived locations, so a root-level comparison alone is incomplete.

Root identity and field meaning need separate comparisonsA namespace identifier selects a root. Field layout determines positions below that root. A populated-state rehearsal checks whether those positions retain the intended application meaning after the upgrade.Namespace rootField locationsState meaningOne unchanged layer cannot certify the next
A root match is evidence for identity, while compatibility and accounting require their own checks.
Root identity
The namespace identifier and the actual location used by storage access.
Layout identity
Field type, order, packing and derived locations under the pinned compiler.
Meaning preservation
The balances, permissions and accounting relationships expected by the application after its release.

OpenZeppelin's upgrade validators recognize namespace identifiers and apply storage modification checks within each recognized namespace. Namespaced layout does not exempt a structure from those checks. The cited tools require Solidity 0.8.20 or later because they need the compiler information supporting this analysis. An annotation in a source file cannot provide missing compiler output.

Appending a field under the documented compatibility rules differs from inserting a field before existing members. Even an allowed structural addition needs application tests for its default state and initialization path. A new configuration field might be structurally harmless yet select an unintended behavior when left at its default value.

Removed state does not automatically disappear from storage. Later code can read words that an earlier implementation stopped naming. If deletion or reuse is part of the intended migration, specify the transformation and test it explicitly. Do not describe a source-level deletion as an on-chain erasure.

Keep dependencies and application structures in the same comparison packet. Our census found unchanged normalized declarations for retained library identifiers, but that says nothing about a project's own namespace, inherited custom assembly or changed interpretation of a stored parameter. Those paths need their own evidence.

Inspect the actual storage access path

ERC7201's annotation documents a claim; the developer remains responsible for implementing it. Inspect the helper that returns the storage pointer and every custom path that can read or write the namespace. A correct annotation beside an incorrect assembly assignment leaves tooling and runtime behavior describing different locations.

Formula identifier and namespace identifier have separate meanings in the annotation. The former names the convention used to derive a location; the latter names the structure's namespace. A review record should preserve both, plus the actual root literal or computation used by the implementation. Comparing only the final label can miss a changed access helper.

  1. Inventory the storage-bearing contracts and bases actually present in the deployed inheritance graph.
  2. Pair each annotation with its structure and pointer helper. Check that the helper accesses the declared root.
  3. Identify custom assembly, manually calculated slots and delegated code that can share the storage context. Record paths the automatic validator does not establish.
  4. Compare old and new compiler layout artifacts using the pinned configuration. Keep namespace-level findings separate from ordinary layout findings.
  5. Explain every intentional migration with preconditions and postconditions, then test the exact release payload against populated state.

An identifier must not be repeated within a contract or its base contracts under the cited OpenZeppelin guidance. That is an application graph rule. Our package-wide uniqueness observation is a different count: it describes the published tarballs and cannot prove that a custom inheritance graph has no duplicate identifiers.

Annotated structures outside a contract are not treated as that contract's namespaces by the standard's stated convention. That boundary matters for an inventory script: a convenient lexical match is not always a semantic namespace belonging to the deployed contract. Our census retains its per-file records so a reviewer can inspect classification rather than trusting a total alone.

Also distinguish persistent namespaces from transient storage. The namespace census is not a claim about transaction-lifetime slots. A proxy's delegated code can share a storage context, but persistent layout and transient lifetime impose different review obligations. Combining the two in a single storage-safe label obscures which properties were actually checked.

For independent review, a scope built from these access paths is more useful than an instruction to check ERC7201. The member directory and Pharos Production profile can support provider evaluation. Require the report to name custom paths and excluded transformations alongside the tool-generated layout result.

Rehearse the upgrade on populated state

An empty mapping cannot demonstrate that an old position remains reachable. Populate fixtures representing the application's meaningful states before applying the new implementation. Include balances, roles, packed fields and nondefault configuration that an empty deployment would conceal. A fork can add realistic state, but its selected block may omit important boundaries.

State fixtures and the assertion they should preserve
FixturePost-upgrade assertionFailure it can expose
Several mapping keysEach intended position remains reachableChanged root or key derivation
Nondefault packed fieldsValues retain their separate meaningsChanged offsets or decoding
Existing authorized actorsOnly intended authority remainsRole or owner state read from another location
Partially initialized extensionMigration follows the approved state transitionRepeated or omitted initialization

Initialization is part of the storage transition. Upgradeable initializers require explicit parent initialization according to the cited guidance. Field declaration initialization behaves like constructor initialization and does not initialize proxy storage. A release packet that presents a new field's source default as deployed state has skipped the relevant execution step.

The guidance advises locking an implementation with _disableInitializers in its constructor. Locking the standalone implementation and initializing the proxy are separate operations. Review both the standalone implementation and the proxy's initialization or reinitialization payload, then keep their results separate in the acceptance record.

Reinitializers are excluded from initializer validation by default in the cited tools. If the intended migration relies on one, explain how it is reviewed and tested. A clean ordinary initializer validation result does not establish that an omitted migration was correct. Record the actual payload, its authorized caller and the state it is expected to change.

A useful rehearsal asserts meaning after the exact upgrade action and after an ordinary user operation. Reading expected values immediately after migration is necessary, but a later withdrawal, role change or accounting update can expose a path the initial check never exercised. Choose the follow-up operation from the contract's real behavior instead of adding generic tests unrelated to its stored state.

For accounting contracts, derive those assertions from the protocol invariants guide. The invariant should identify the units and relationships preserved by the upgrade. A slot-level match can coexist with a changed interpretation of shares or liabilities, so the release needs both structural and economic assertions.

When a test fails, preserve the first differing observation. Determine whether the namespace root changed, field decoding changed or application logic used the same value differently. That classification gives the next investigation a concrete object. A generic upgrade-failed label cannot tell the reviewer which layer needs correction.

A concrete ERC20 rehearsal makes the distinction inspectable. The retained package's ERC20 namespace holds balances, allowances, total supply, name and symbol. Seed more than one account and an allowance that permits a bounded transfer. Capture those reads under the old implementation, apply the exact upgrade payload and compare the same reads under the new implementation before performing further work.

Then exercise transferFrom with the intended spender. Check the recipient and owner balance changes, allowance consumption and supply relationship. The expected outcome must follow the composed token's own behavior; custom update hooks can introduce additional effects that the base-library structure does not establish. This is a proposed application rehearsal, not an executed result of the package census.

Use an unlimited-allowance fixture separately. The pinned base implementation does not decrease an allowance equal to the uint256 maximum. A test that expects every successful transferFrom to decrease allowance would reject correct base behavior. Conversely, a custom override needs its own expectations. Record which implementation supplied the assertion rather than treating the familiar ERC20 label as a complete specification.

A namespace rename can make those earlier balances and allowances appear absent even while their storage words remain present at the previous root. The rehearsal should distinguish an unreachable old value from an intentional migration to a new location. If migration is intended, identify where the old value is read, how the new value is written and which authority can execute that transformation.

Preserve the accepted fixture after the review. A later dependency update can reuse it to compare both reads and ordinary token operations. The complete package inventory tells the reviewer which annotated identifiers or normalized declarations changed; the populated fixture tells the maintainer whether this application's meaning survived its actual release.

Accept the release from a storage evidence packet

The packet should identify package versions, compiler configuration, deployed inheritance, namespace inventory, actual access helpers and the exact upgrade payload. Include the validator output and the populated-state rehearsal results. Document custom accesses or transformations that remain outside automatic validation instead of treating them as implicitly approved.

Separate the census result from the release verdict. We observed 20 added identifiers and no removed identifiers between the two complete packages. That is reproducible evidence about those packages. The application may import none of the additions or may introduce additional namespaces of its own; its deployment decision follows its own graph.

Use an explicit change trigger for a dependency update. An unchanged namespace identifier should prompt a field and access comparison, not terminate the review. A changed identifier should prompt a migration question. When the root and fields match, the next question is whether application meaning and initialization still match.

The inventory remains valuable because it is complete, versioned and replayable. Its limits remain equally important. A maintainer can approve a release only after connecting those observations to the contract's actual storage paths and preserving the state that its users depend on.

Frequently asked questions

Do diamond contracts have to adopt ERC7201?

The standard says namespaced storage and the diamond proxy pattern can be used independently. Review the implementation's actual storage convention rather than infer it from the proxy pattern's name.

Can a constants-only helper file count as an annotated namespace?

A root constant alone does not establish an annotated storage structure. The census records Solidity files separately from annotation-bearing files so those denominators are not confused.

Is a namespace census sufficient for a dependency procurement decision?

It provides a versioned inventory. Licensing, maintenance, compiler support and the application's actual use remain separate decisions; the count is not a vendor quality or security score.

Comments

0

    Leave a comment

    Share a question or observation about this article.

    10 to 3,000 characters.