ERC-8110 - Domain Architecture for Diamonds

Created 2025-12-20
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This standard introduces a domain-based architectural pattern for contracts implementing the Diamond execution model defined by ERC-2535 (Diamond Standard) or ERC-8109 (Diamond, Simplified), together with the storage identifier mechanism defined by ERC-8042 (Diamond Storage Identifier).

It defines a domain-centric storage management architecture, providing consistent storage identifiers and a structured directory model that decouples storage ownership from facet logic.

This pattern helps reduce storage collisions and human error while enabling better tooling for multi-facet systems.

Motivation

ERC-2535 provides a flexible foundation for modular smart contracts through facets, but it intentionally leaves storage organization and architectural conventions open to implementation.
While this flexibility encourages creativity, it can sometimes lead to inconsistency.
Each developer or team may structure storage differently, making it harder to design robust and easy-to-use tooling for storage management.

Without a shared structural framework, storage identifiers may be inconsistently verified or reused across facets, which can result in unexpected collisions or subtle upgrade issues between facets sharing the same state.

ERC-8042 introduced human-readable storage identifiers to improve clarity, but it does not define how those identifiers should be structured or grouped in larger projects.

This EIP proposes a domain-centric architectural pattern that establishes a consistent framework for managing storage independently of facet implementation.

By introducing clear domain boundaries and deterministic naming rules for storage identifiers, the pattern maintains the openness of the Diamond Standard while providing a shared foundation for collaboration, tooling support, and long-term upgrade safety across complex systems.

Specification

1. Domain Definition

2. Storage Identifier Naming Convention

A storage identifier is the human-readable string whose keccak256 hash defines a Diamond Storage position.

    bytes32 constant STORAGE_POSITION = keccak256("meaningful.string");

It represents the domain that owns and manages a specific storage layout.
To ensure uniqueness and clarity, at a minimum, a storage identifier SHOULD include the following components:

    {project}.{domain-name}.{version}

To improve readability, namespace separation, and tooling support, additional contextual components MAY be included, resulting in the following extended format:

    {org}.{project}.{domain-type}.{domain-name}.{version}

Identifier Components

Each domain:

Sub-domain Components

A sub-domain represents a storage-isolated vertical extension of an existing domain.

A sub-domain is used when new functionality belongs conceptually to an existing domain, but its required state can be cleanly isolated without modifying or appending to the original domain’s storage layout.

If present, the sub-domain's identifier format becomes:

    {project}.{domain-name}.{version}.{sub-domain}

or, when using the extended format:

    {org}.{project}.{domain-type}.{domain-name}.{version}.{sub-domain}

The version component indicates the domain version in which the sub-domain was introduced.
It serves as a historical and organizational reference, not as an independent versioning lifecycle for the sub-domain.

Definition and Rules

A sub-domain:

Sub-domains exist as a safety-oriented design choice to isolate newly introduced state, while preserving the original domain layout unchanged.

Choosing between evolving the existing domain storage or introducing a sub-domain depends on the project’s complexity, the team’s discipline, and long-term maintenance goals.

3. Storage Declaration Requirements

To support reliable tooling and explicit storage ownership, each domain defined by this proposal MUST declare its storage location using the ERC-8042 NatSpec annotation.

Specifically, the domain-owned storage struct MUST be annotated with:

    ///@custom:storage-location erc8042:<NAMESPACE_ID>

This proposal does not redefine the storage location formula, but requires the use of this annotation to ensure that domain storage is discoverable, unambiguous, and machine-readable.

Example Identifiers

The extended identifier format is recommended for global uniqueness. It is especially useful when integrating shared libraries, predefined facets, or other standards, where namespace collisions are more likely.

For application-specific systems, teams may choose a minimal identifier format to reduce naming complexity, as long as the identifier remains stable and unique within the project.

Equipment Identifier

Represents a business domain responsible for equipment state. This domain has undergone a layout-breaking change, therefore uses an explicit v2 identifier.

    /// @custom:storage-location erc8042:org.project.business.equipment.v2
    /// @dev Minimal form: project.equipment.v2
    bytes32 constant EQUIPMENT_STORAGE_POSITION = keccak256("org.project.business.equipment.v2");

Character Identifier

Represents a business domain responsible for character state and progression. This example also demonstrates how a domain can be extended using a storage-isolated sub-domain.

    /// @custom:storage-location erc8042:org.project.business.character.v1
    /// @dev Minimal form: project.character.v1
    /// @dev Main character domain.
    bytes32 constant CHARACTER_STORAGE_POSITION = keccak256("org.project.business.character.v1");

    /// @custom:storage-location erc8042:org.project.business.character.v1.mounted
    /// @dev Minimal form: project.character.v1.mounted
    /// @dev Sub-domain for global character mounted state.
    bytes32 constant CHARACTER_MOUNTED_STORAGE_POSITION = keccak256("org.project.business.character.v1.mounted");

Game Setting Identifier

Defines a system-level domain for game-wide configuration shared across multiple facets.

    /// @custom:storage-location erc8042:org.project.system.gamesettings
    /// @dev Minimal form: project.gamesettings
    bytes32 constant GAME_SETTING_STORAGE_POSITION = keccak256("org.project.system.gamesettings");

4. Directory Convention

In line with Domain-Driven Design principles, the directory layout SHOULD reflect domain ownership.

Example Directory

This directory structure is illustrative and does not mandate a specific naming convention.
Subdirectory names such as storage/ are illustrative and may contain both storage layout definitions and internal domain logic.

contracts/
├── diamond/
│   └── Diamond.sol
│
├── character/
│   ├── storage/
│   │   └── CharacterStorage.sol
│   └── facets/
│       └── CharacterFacet.sol
│
├── equipment/
│   ├── storage/
│   │   └── EquipmentStorage.sol
│   └── facets/
│       └── EquipmentFacet.sol
│
└── gamesettings/
    ├── storage/
    │   └── GameSettingsStorage.sol
    └── facets/
        └── GameSettingsFacet.sol

5. Upgrade Scenarios

This architecture defines upgrade behavior based on the effect new selectors introduce to domains and storage, rather than on facets themselves.

Upgrades fall into one of the following cases.

Case 1: No new storage required

If new selectors do not require any additional storage:

This is the simplest and safest upgrade path, as it introduces no new state and does not affect existing storage layouts.

Case 2: New domain required (horizontal upgrade)

If new functionality introduces state that does not logically belong to any existing domain:

This represents a horizontal expansion of the system, allowing new features to be introduced without impacting existing domains or storage layouts.

Case 3: New variables within an existing domain (vertical upgrade)

If new selectors require additional state that logically belongs to an existing domain, and the existing storage layout is not broken, this becomes a design trade-off.

Two common approaches MAY be used:

Option A — Evolve the existing domain

This keeps the domain unified and works well for tightly coupled or complex business logic.
It requires strict discipline when managing storage layout.

Option B — Introduce a sub-domain

This approach reduces risk and cognitive load by isolating newly introduced state, while keeping the original domain layout stable.

The choice between these approaches depends on project complexity, team discipline, and long-term maintenance goals.

Case 4: Layout-breaking change

If new selectors require a change that breaks the existing storage layout of a domain
(for example, changing the inner structure of nested structs or struct arrays)

This architecture does not attempt to automate or abstract storage migration.
The goal is to keep schema changes intentional, visible, and auditable.

If the layout-breaking change is partial, and the newly required state can be cleanly isolated and defined independently, developers MAY also consider introducing a sub-domain instead of versioning the entire domain.

Rationale

From the beginning, the Diamond Standard (ERC-2535) was designed around the relationship between function selectors and storage positions, not around facets themselves.
Facets are replaceable units of logic — the diamondCut operation only replaces, removes or adds code — but the storage layout persists and defines the actual state continuity of the contract.

ERC-8109 itself only defines introspection mechanisms for querying the Diamond and deliberately leaves the upgrade mechanism to implementations or to other standards.
This flexibility is achieved because both introspection facets and upgrade facets interact with the same underlying domain (8109.diamond).

This demonstrates that storage belongs to the domain, not the facet, facets merely provide interfaces for logic to read or mutate that domain.

Over time, many implementations have treated facets as the primary boundary of responsibility, grouping logic and storage together without recognizing that storage domains are the true architectural anchors.
This misunderstanding leads to inconsistent storage management, overlapping identifiers and fragile upgrade paths where one facet unintentionally corrupts another’s state.

The domain-centric approach restores the original intent of the Diamond:
Selectors (facets) operate through domains, not as domains.
Each domain defines its own persistent storage struct and identifier, while facets merely act as interfaces that execute logic against it.

This shift decouples storage from logic when separation is desired, while still allowing tightly coupled designs when intentional.
It enables:

By formalizing this pattern, Diamond Architecture becomes safer, more transparent and easier to extend — re-aligning practice with its original design philosophy.

Special Case: Domain–Facet Overlap

There is a special case within the separation principle where a domain (or sub-domain) and its facet are intentionally designed to represent the same logical entity.

In this scenario, the facet implements all functions belonging to its domain. This reduces flexibility, but improves encapsulation and self-containment, making the facet behave like a reusable application module rather than a low-level primitive.

A concrete example of this approach can be found in the Compose project.
In Compose, a facet and its associated sub-domain are explicitly mapped into a single entity, enabling the creation of predefined, plug-and-play standard facets.

This approach is suitable for systems that prioritize modular composition and standardized functionality, allowing developers to safely integrate common features with predictable behavior.
However, when implementing custom or project-specific logic, domains and facets SHOULD still be treated as separate entities.

Maintaining this separation preserves clarity of ownership, supports future upgrades, and supports the long-term evolution of application-level Diamond architectures.

Sub-domains and Layout-Sensitive State

Sub-domains can also serve as a practical way to isolate layout-sensitive state.

Projects that need to move quickly may choose to place complex or layout-unstable data (such as mappings or dynamic arrays) in a primary domain, while isolating smaller or more compact state in sub-domains.

As development progresses, additional state can either be appended to an existing domain or introduced via a sub-domain, depending on data shape and evolution needs.

This allows projects to start with a simple structure while preserving flexibility to refine storage organization as the system scales.

Isolated Domain

An isolated domain describes a conceptual separation between a domain and function- or facet-level logic.

In this model, a domain defines its own storage access helpers. Facets and their functions interact with domain-owned state through these helpers, rather than accessing storage layouts directly.

This approach makes data access logic explicit at the domain level, while allowing facets to focus on business logic and coordination.

A domain may be fully isolated, partially isolated, or not isolated, depending on project needs.

This concept is particularly useful when functions or facets need to coordinate state across multiple domains or sub-domains, as it helps keep cross-domain interactions consistent and easier to reason about.

Backwards Compatibility

This proposal is fully backward-compatible with ERC-2535 (Diamond Standard), ERC-8042 (Diamond Storage Identifier) and ERC-8109 (Diamond, Simplified). It introduces no breaking changes, no new opcodes, and no modifications to existing protocol mechanics.

It does not alter the execution model defined by ERC-2535 or ERC-8109. The relationships between facets, selectors, and shared storage remain unchanged and fully compatible across all three standards.

Instead, this proposal defines an architectural convention that complements existing Diamond standards by:

This architecture may be applied to contracts implementing either ERC-2535 or ERC-8109, as both share the same fundamental facet and selector architecture.

Developers are encouraged to continue following all applicable standards to maintain interoperability while benefiting from clearer state ownership, reduced storage collision risk, and lower architectural complexity.

Adoption for Deployed Systems

For already deployed systems, adoption can be done incrementally.

The Domain Architecture does not require projects to modify, rename, or refactor existing libraries or other standards in order to adopt it. If a library or standard already follows ERC-2535, ERC-8042, or uses its own established storage identifiers, that code SHOULD remain unchanged.

Adoption MAY begin at the application layer, without touching shared libraries or standardized components. Existing storage identifiers MAY be treated conceptually as pre-v1 domains.

In practice, projects typically start by:

Adopting the Domain Architecture does not require migrating existing state. It primarily affects how new storage is introduced and how future upgrades are structured.

When a layout-breaking change is required, a new versioned storage identifier can be introduced explicitly, allowing existing storage layouts to remain untouched. Any data migration, if needed, MUST be handled explicitly by the project.

The primary consideration during adoption is identifier uniqueness. New storage identifiers MUST NOT collide with existing identifiers from shared libraries or from within the project itself.

Reference Implementation

Minimal implementation examples demonstrating the convention:

Business Domain (Equipment)

    /// @custom:storage-location erc8042:org.project.business.equipment.v2
    /// @dev Minimal form: project.equipment.v2
    bytes32 constant EQUIPMENT_STORAGE_POSITION = keccak256("org.project.business.equipment.v2");

    struct Equipment 
    {
        uint8 itemType;        
        uint16 power;
        uint8 rarity;
        address effectOwner;   
    }

    struct EquipmentStorage 
    {
        mapping(uint256 => Equipment) items; // itemId => Equipment
    }

    function equipmentStorage() pure returns (EquipmentStorage storage s) 
    {
        bytes32 position = EQUIPMENT_STORAGE_POSITION;
        assembly {
            s.slot := position
        }
    }

Business Domain (Character)

    /// @custom:storage-location erc8042:org.project.business.character.v1
    /// @dev Minimal form: project.character.v1
    /// @dev Main character domain.
    bytes32 constant CHARACTER_STORAGE_POSITION = keccak256("org.project.business.character.v1");

    struct Character 
    {
        uint32 level;
        uint256 hp;

        // equipment slot => itemId
        // e.g. slot: head, chest, weapon, boots...
        mapping(uint8 => uint256) equippedItemId;
    }

    struct CharacterStorage 
    {
        mapping(uint256 => Character) characters;
    }

    function characterStorage() pure returns (CharacterStorage storage s)
    {
        bytes32 position = CHARACTER_STORAGE_POSITION;
        assembly {
            s.slot := position
        }
    }

    /// @custom:storage-location erc8042:org.project.business.character.v1.mounted
    /// @dev Minimal form: project.character.v1.mounted
    /// @dev Sub-domain for global character mounted state.
    bytes32 constant CHARACTER_MOUNTED_STORAGE_POSITION = keccak256("org.project.business.character.v1.mounted");

    struct CharacterMountedStorage {
        // Global character state, persists across character switches
        bool isMounted;
    }

    function characterMountedStorage() pure returns (CharacterMountedStorage storage s)
    {
        bytes32 position =  CHARACTER_MOUNTED_STORAGE_POSITION;
        assembly {
            s.slot := position
        }
    }

System Domain (Game Settings)

    /// @custom:storage-location erc8042:org.project.system.gamesettings
    /// @dev Minimal form: project.gamesettings
    bytes32 constant GAME_SETTING_STORAGE_POSITION = keccak256("org.project.system.gamesettings");

    struct GameSettingStorage {
        uint256 balancePatchBlock;
        bytes32 rulesetHash;
        uint32 gameVersion;
        uint32 seasonId;
        bool tradingEnabled;
        bool craftingEnabled;
        bool pvpEnabled;
    }

    function gameSettingsStorage() pure returns (GameSettingStorage storage s) {
        bytes32 position = GAME_SETTING_STORAGE_POSITION;
        assembly {
            s.slot := position
        }
    }

Each domain defines and owns its storage independently. Facets interact with domain-owned storage definitions, supporting safe upgrades and avoiding unintended storage overlap.

Security Considerations

This pattern strengthens the security model of Diamond-based systems by introducing explicit and deterministic storage identifiers.

By separating domains and enforcing consistent naming rules, it reduces the risk of:

Each domain owns its ERC-8042 storage identifier. When combined with append-only storage layout upgrades, this allows storage evolution without interfering with existing state.

This clarity also improves auditability and supports static analysis tooling when analyzing storage safety across upgrades.

Copyright

Copyright and related rights waived via CC0.