All guides

Diamond Storage Patterns: AppStorage vs Diamond Storage

Why facets cannot use ordinary state variables, and how Diamond Storage and AppStorage keep upgradeable contracts from corrupting their own state.

11 min read

Storage is where diamonds get genuinely subtle. If you take one thing from this guide, make it this: a facet's state variables do not belong to the facet — they belong to the diamond, and every facet shares the same storage space.

Why ordinary state variables break

Solidity assigns storage slots by declaration order: the first variable goes in slot 0, the next in slot 1, and so on. Each contract is compiled independently, with no knowledge of the others. Now consider two facets on the same diamond:

// BROKEN — do not do this in a facet
contract StakingFacet {
    uint256 public totalStaked;              // slot 0
    mapping(address => uint256) public stakes; // slot 1
}

contract RewardsFacet {
    address public rewardToken;              // slot 0  <-- same slot!
    uint256 public rate;                     // slot 1  <-- same slot!
}

Because both facets execute via delegatecall against the diamond's storage, totalStaked and rewardToken are the same 32 bytes. Staking would overwrite the reward token address with a number; setting the reward token would corrupt the staked total. The compiler cannot warn you, because from its point of view these are two unrelated contracts.

The fix is to stop letting the compiler choose slots and instead place state at addresses you control. There are two established ways to do that.

Pattern 1: Diamond Storage

Each module picks a unique, human-readable namespace, hashes it to get a storage slot far away from slot 0, and anchors a struct there using inline assembly:

library LibStaking {
    // A namespaced, effectively-random slot. Nothing else lands here.
    bytes32 constant STORAGE_POSITION = keccak256("com.myprotocol.staking.storage");

    struct StakingStorage {
        uint256 totalStaked;
        mapping(address => uint256) stakes;
    }

    function s() internal pure returns (StakingStorage storage ss) {
        bytes32 position = STORAGE_POSITION;
        assembly {
            ss.slot := position
        }
    }
}

contract StakingFacet {
    function stake(uint256 amount) external {
        LibStaking.s().stakes[msg.sender] += amount;
        LibStaking.s().totalStaked += amount;
    }
}

Because the slot is a keccak-256 hash, the chance of two namespaces colliding is negligible, and the chance of colliding with the compiler's sequentially-allocated slots is effectively zero.

Strengths

  • Modules are fully independent — a facet only touches storage it explicitly opts into.
  • You can add a new module later without thinking about existing layout at all.
  • Facets are genuinely reusable across different diamonds.

Costs

  • More boilerplate: every module needs a library and an accessor.
  • Sharing data between modules means importing several libraries, which gets verbose quickly.

Pattern 2: AppStorage

AppStorage takes the opposite approach: one struct for the entire protocol, declared as the first and only state variable in every facet.

// AppStorage.sol — one shared struct for the whole protocol
struct AppStorage {
    uint256 totalStaked;
    mapping(address => uint256) stakes;
    address rewardToken;
    uint256 rate;
}

// Every facet declares it FIRST and declares nothing else.
contract StakingFacet {
    AppStorage internal s;   // occupies slot 0 onwards

    function stake(uint256 amount) external {
        s.stakes[msg.sender] += amount;
        s.totalStaked += amount;
    }
}

contract RewardsFacet {
    AppStorage internal s;   // same layout, same slots — intentionally

    function setRate(uint256 r) external {
        s.rate = r;
    }
}

Every facet now agrees on the layout because they all include the same struct definition. The variable name s is a convention that makes it obvious at a glance that you are touching shared state.

Strengths

  • Much less boilerplate; reads like ordinary Solidity.
  • Any facet can reach any field without importing a library per module.

Costs

  • Every facet must declare AppStorage first and declare no other state variables. One slip corrupts everything.
  • The struct becomes a large shared surface with no module boundaries.
  • All facets must be recompiled against the same struct version.

The upgrade rule that actually bites

Whichever pattern you choose, the rule for changing a storage struct is the same: you may append, but you may never insert, reorder, or remove.

// Version 1
struct AppStorage {
    uint256 totalStaked;
    mapping(address => uint256) stakes;
}

// Version 2 — SAFE: new field appended at the end
struct AppStorage {
    uint256 totalStaked;
    mapping(address => uint256) stakes;
    uint256 lastUpdated;   // new slot, previously unused
}

// Version 2 — UNSAFE: field inserted in the middle
struct AppStorage {
    uint256 totalStaked;
    uint256 lastUpdated;   // shifts every later field by one slot
    mapping(address => uint256) stakes;
}

The unsafe version does not fail loudly. It silently reinterprets whatever was in the old slot as the new field, so every existing user's stake becomes garbage. There is no automatic migration and no way to detect it after the fact except by noticing that the numbers are wrong.

Changing a field's type is equally dangerous even when the slot count stays the same, and so is anything that alters struct packing — narrowing a uint256 to uint128 lets the next field share the slot and shifts everything after it.

Nested structs and storage gaps

A struct nested inside another struct cannot be extended, because its fields are laid out inline and growing it would shift everything after it. If you expect a nested struct to grow, reserve space up front:

struct StakingStorage {
    uint256 totalStaked;
    mapping(address => uint256) stakes;
    // Nested structs cannot be safely extended later unless
    // you leave room, so reserve slots up front:
    uint256[45] __gap;
}

A gap costs nothing until used — unwritten slots consume no gas and no storage — so it is cheap insurance.

Which pattern should you use?

For a protocol built and upgraded by one team, AppStorage is usually the pragmatic choice: less ceremony, faster to write, easier to read in review. For facets shared across diamonds, for modules owned by separate teams, or for anything where a third party might add a facet later, Diamond Storage's isolation is worth the boilerplate.

Mixing them is common and perfectly legitimate: AppStorage for core protocol state, Diamond Storage for self-contained utilities like ownership, pausing or access control. That is exactly how the reference implementations structure things.

Verifying storage safety before you ship

  • Run forge inspect <Facet> storage-layout (or the solc --storage-layout output) for every facet and diff it against the previous release.
  • Assert in review that no facet declares a state variable other than the shared struct, if you use AppStorage.
  • Fork-test the upgrade against real mainnet state and read back known values afterwards. This catches layout mistakes that unit tests on a fresh deployment never will.

Storage bugs are the most expensive class of diamond bug because they are silent and often irreversible. The security checklist covers the review steps in more depth, and upgrading with diamondCut explains how to run migrations atomically with the upgrade itself.