Storage Layout Collision / Proxy State Aliasing

  • Name: Storage layout collision / proxy state aliasing
  • Category: exploit pattern / upgradeability failure / storage-domain collision / proxy metadata aliasing
  • Summary: A storage-layout collision happens when two parts of a contract system believe the same slot means different things. The most dangerous version shows up in proxy architectures: proxy metadata, implementation state, inherited storage gaps, custom storage namespaces, or upgraded layouts alias the same storage word, so safety-critical checks start reading attacker-useful garbage as if it were legitimate state. The exploit is rarely read random slot, drain funds in one step. More often the collision corrupts a root-of-trust primitive first: an initializer guard, owner field, implementation pointer, governance address, pause flag, balance accumulator, or accounting index. Once that invariant is false, some higher-level privileged path becomes exploitable.
  • What it is:
    • A system relies on multiple contracts, implementations, or inheritance branches sharing one storage namespace through delegatecall or upgrade compatibility assumptions.
    • Two components disagree about the meaning, packing, or position of one or more storage slots.
    • A write that is safe in one component becomes a silent corruption of security-critical state in another.
    • The attacker then invokes whichever function now trusts the corrupted state: re-initialization, upgrade, governance, withdrawal, mint, or solvency logic.
  • Typical preconditions:
    • The protocol uses a proxy, diamond, plugin, library-delegatecall, or upgrade pattern where code and storage ownership are separated.
    • Developers customize proxy admin / implementation storage rather than isolating it in reserved slots like EIP-1967 or ERC-7201-style namespaces.
    • An upgrade, inheritance change, or hand-written storage struct moves variables without preserving prior layout assumptions.
    • Safety checks depend on packed booleans, counters, or pointers whose exact slot placement matters.
    • The corrupted field gates privileged behavior or accounting truth once read through the implementation context.
  • Common vulnerable surfaces:
    • custom upgradeable proxies that store admin, implementation, or module metadata in ordinary slot 0, 1, or nearby implementation slots
    • inherited Initializable, Ownable, pause, or reentrancy state that assumes a different slot map than the proxy actually exposes
    • upgrades that reorder, remove, or repack storage variables without a migration-safe storage gap discipline
    • diamond / facet systems where shared AppStorage and facet-local assumptions diverge
    • delegatecall-based module systems that treat library code as if it had its own isolated storage
    • structs or tightly packed booleans where one non-zero byte flips a guard far away from the value developers thought they were writing
  • Exploit sequence:
    1. The attacker identifies a state variable whose effective meaning changes across proxy and implementation contexts.
    2. A legitimate existing value in that slot, or an attacker-triggerable write into that slot, makes a privileged flag or pointer appear valid when it should not.
    3. The implementation reads corrupted state and authorizes a dangerous action: initialization, ownership change, governance power rewrite, arbitrary upgrade, or faulty accounting.
    4. The attacker uses that newly opened control path to seize authority or turn fake balances into real withdrawals.
    5. The visible drain often happens one layer above the original bug, which is why storage collisions are easy to mislabel as simple governance or access-control failures.
  • Variants:
    • Proxy-metadata collision — admin or implementation pointers stored in ordinary slots alias implementation-side auth or initializer state, as in Audius.
    • Upgrade layout drift — a new implementation reorders or inserts variables so old live state is reinterpreted under new logic, including rushed incident-response patches that move initializer or pause-related fields without preserving old slot meaning, as in Pike.
    • Packed-flag contamination — one byte of an unrelated address, counter, or enum flips a boolean guard because both share a slot.
    • Facet / module namespace collision — diamond or plugin modules assume isolated storage but actually overwrite each other’s control state.
    • Split-namespace authority drift — different reachable initializers or auth paths each believe they check the canonical guard, but they read different custom storage namespaces while still mutating the same live authority fields, as in DeltaPrime.
    • Migration corruption — a one-off migration script writes compatibility shims into slots later treated as permanent protocol truth.

Three storage-authority promotions reviewers should not collapse

  1. Some live slot value or write path exists at all

    • A proxy, implementation, facet, migration helper, or packed struct exposes bytes that can be read or mutated through some reachable path.
    • That is only the weakest fact: this storage word can hold something or this code path can still write it.
  2. Those bytes still count as the one canonical meaning for this exact guard or authority field

    • The harder question is whether every code path agrees on what slot 0x... means, how it is packed, and which namespace or inheritance branch defines truth for initialized, owner, admin, implementation, pause, or accounting state.
    • Most failures in this family happen here: the protocol promoted bytes exist in this slot into these bytes still mean the same security-critical thing everywhere that matters.
  3. That accepted meaning deserves this much live authority right now

    • Even if a field decodes cleanly, healthy systems still should not let one freshly reinterpreted slot immediately inherit unlimited upgrade, governance, withdrawal, mint, or liquidation power without blast-radius controls.
    • This is where pause design, market caps, staged rollout, and rapid containment stop a storage-authentication mistake from becoming an instant custody event.

Healthy systems keep those three promotions separate instead of flattening them into one vague comfort claim like the guard is there or the slot still exists.

  • Detection heuristics:
    • Map storage layout across proxy and implementation together, not separately; every delegatecall system has only one real storage plane.
    • Diff old and new storage layouts before every upgrade, including inherited bases, packing effects, and storage-gap consumption.
    • Treat custom proxy storage as security-critical design, not innocuous plumbing.
    • Inspect whether any privileged guard depends on packed booleans or sentinel values that can be made truthy by arbitrary non-zero bytes.
    • When a vulnerability looks like initializer guard failed anyway or owner changed mysteriously, ask whether the slot was ever trustworthy in the active execution context.
  • Defenses:
    • Use well-understood reserved-slot patterns for proxy metadata instead of ordinary implementation-adjacent slots.
    • Preserve strict append-only storage discipline across upgrades, with automated layout diffing in CI.
    • Keep security-critical flags in clearly namespaced storage rather than relying on fragile packing coincidences.
    • Review inheritance trees and storage gaps as part of security review, not just functionality review.
    • For modular systems, give each facet or library an explicit storage namespace and test for cross-module slot collisions.

What sound design looks like

  • Healthy systems keep three facts separate on purpose: some live slot value or write path exists, those bytes still count as the one canonical meaning for this exact guard or authority field, and that accepted meaning deserves this much live authority right now. The slot is there is only the beginning of review, not the end of it.
  • The system has one canonical storage story across proxies, implementations, facets, plugins, and migrations. Reviewers can point to one authoritative map of what each live slot means before and after change, rather than reconstructing that truth from scattered code paths.
  • Proxy metadata and privilege-critical implementation state live in deliberately separated namespaces. Admin pointers, initializer bits, pausers, and accounting indices do not rely on low-slot convenience or incidental packing behavior.
  • Upgrades preserve semantic continuity, not just compilation success. A healthy release process proves that the new code will interpret existing live bytes with the same intended meaning, or performs an explicit migration that is bounded, reviewed, reversible, and tested against production-shaped state snapshots before rollout.
  • Bootstrap-only selectors, initializers, and migration helpers are extinguished once rollout is complete, or are hard-scoped so they cannot ever run against live user storage through fallback routing, delegatecall, or beacon dispatch.
  • Security-critical guards are resilient to garbage and aliasing. initialized, owner, and pause truth should not become attacker-usable because one unrelated non-zero byte happened to share a slot, nor because two different code paths quietly authenticate against two different namespaces for the same authority bit.
  • When layout certainty is lost, the protocol treats that as an authorization incident and fails closed through pause, quarantine, or cap reduction before corrupted state can be monetized.

Failure-prevention checklist

  • Treat storage review as three separate approvals, not one vague comfort check: this slot or write path exists, it still means the same canonical thing everywhere that matters, and if we trust that meaning it is allowed to carry this much live authority. Any slot that can influence initialized, owner/admin identity, implementation selection, pause state, or accounting truth belongs inside the protocol’s security model.
  • Keep proxy metadata, facet state, module bookkeeping, and implementation storage in explicit reserved namespaces. Ordinary low slots, hand-rolled packing, and undocumented sentinel reuse should be presumed unsafe until proven otherwise.
  • Require machine-checked storage-layout diffs before every upgrade, hotfix, and emergency migration. Review inherited bases, struct packing, consumed storage gaps, renamed variables, and namespace constants, not only top-level field order.
  • Bind upgrade approval to a specific storage-impact review. A signer approving new bytecode should also be approving the exact claim that live slots keep the same meaning after deployment.
  • Make initializer and auth guards robust against non-zero garbage. If one byte from an unrelated address, counter, or proxy field can flip a boolean truthy, the guard is too fragile for shared storage.
  • Never assume emergency patches are exempt from layout discipline. Incident-time upgrades should fail CI if they reopen initialization, corrupt pause state, or reinterpret live balances under a new slot map.
  • Retire rollout-only selectors and initializers after deployment, and verify that every still-reachable setup or auth path reads the same canonical initialization / authority namespace before it can mutate owner, admin, or implementation state.
  • Give each delegatecall module one explicit storage home and test cross-module writes adversarially. Library code and facet code do not get their own state just because the architecture diagram says they are separate.
  • Monitor production for impossible storage-driven state transitions: post-launch initialization events, owner/admin changes without governance intent, implementation changes coupled to unplanned storage writes, or pause flags that flip after unrelated admin actions.
  • Keep blast-radius containment ready anyway. If layout uncertainty is discovered on live contracts, teams should already know how to pause, isolate, or cap the affected markets before a corrupted guard becomes a drain path.

Audit prompts

  • Where is the single authoritative storage map for this system, and does it include proxy metadata, implementation state, inherited bases, storage gaps, and delegatecall modules together rather than as separate diagrams?
  • Which slots gate root authority here: initializer flags, owner/admin fields, implementation pointers, pausers, guardians, accounting indices, or balance accumulators, and what proves those slots retain one meaning across upgrades?
  • Can any existing live value in proxy or module storage make an implementation-side guard appear initialized, uninitialized, paused, or authorized by accident?
  • During the next upgrade, which variables change packing or ordering, and what exact old live bytes will the new logic read from those slots?
  • Do any booleans, enums, or short counters share slots with addresses, hashes, or pointers such that arbitrary non-zero bytes can satisfy a guard the code treats as binary truth?
  • If a deployment-only selector or initializer is replayable in practice, is the root cause really missing access control, or is the guard reading the wrong storage namespace altogether?
  • Which deployment-time selectors, beacon initializers, migration helpers, or direct-call setup functions remain reachable through fallback routing today, and which exact storage namespaces do they read and write if invoked against live proxies or user vaults?
  • What protects against emergency-upgrade layout drift: automated diff tooling, human signoff on gap consumption, shadow-fork tests, or explicit migration assertions on live state snapshots?
  • In diamond, plugin, or library-based systems, what proves two modules cannot both believe they own the same slot range after future feature additions?
  • If operators had to explain a future incident, could they answer what did slot X mean before and after the upgrade? from one canonical document, or only by reverse-engineering deployed bytecode after the fact?

Lessons that should already be standard

  • A slot exists, that slot still means the same thing everywhere, and that accepted meaning deserves live privileged authority are three different claims.

  • Storage collisions are not low-level hygiene bugs; they are often root-of-trust failures wearing an implementation-detail disguise.

  • A correct-looking guard in the wrong storage domain is not a guard. Many replayable initialization and mysterious admin-takeover incidents are storage-authentication failures first and access-control failures second.

  • Append-only except for this hotfix is how live systems get reinterpreted. Emergency upgrades do not get a waiver from storage semantics.

  • Delegatecall does not create separate state universes. If two modules can write through the same storage plane, their slot discipline is part of the protocol’s authorization design.

  • Packed booleans are easy to underestimate. One stray non-zero byte from an unrelated field can be enough to convert false until governance acts into true right now for the attacker.

  • Custom storage namespaces are not a safety blanket by themselves. If two reachable code paths disagree about which namespace defines initialized, owner, or selector authority, the protocol still has one storage-authentication problem.

  • Teams often focus on who can call upgradeTo() and forget to ask what the upgraded code will believe about existing state. That second question is where many real incidents begin.

  • If a protocol cannot state its live storage invariants clearly enough to diff them before an upgrade, it is carrying latent custody risk even before an attacker appears.

  • Postmortems that end at initialize() was callable again are often stopping one layer too late. The deeper question is why storage made that call look legitimate in the first place.

  • High-signal incidents:

    • Audius governance storage-collision re-initialization takeover (2022-07-23) — AudiusAdminUpgradeabilityProxy stored proxyAdmin in slot 0, colliding with OpenZeppelin Initializable booleans so initialize() stayed replayable and governance could be rewritten.
    • re-initialization drain (2024-04-30) — Pike’s own incident notice says the emergency pause upgrade introduced new variables that altered storage layout, moved the effective initialized position, and made live spoke proxies behave as uninitialized, reopening initialize() and then upgradeToAndCall() across three chains.
    • [[../../../incidents/governance-admin-upgrades/deltaprime-prime-account-diamondbeacon-init-selector-reinitialization-hijack-2024-07-23|DeltaPrime Prime Account DiamondBeacon init() selector re-initialization hijack (2024-07-23)]] — DeltaPrime’s beacon-side init() guard read a different custom storage namespace than the Prime Account’s real initializer guard, so a deployment-only selector stayed replayable against already-initialized user vault storage while still overwriting the live account owner field.

Internal linkages

Control surface