ERC-8042: Diamond Storage
ERC-8042 standardises the storage layout that ERC-2535 diamond proxies have been using ad-hoc since 2018: each facet writes to a deterministic, namespace-isolated storage slot computed from a hash of the facet's identifier. This avoids the storage-collision problem that naïve diamond patterns hit — when two facets both think they own slot 0x00, one overwrites the other on upgrade.
The pattern is what ERC-7201 generalised for any upgradeable contract; ERC-8042 specifically formalises it for diamonds.
Required Pattern
library FacetXStorage {
// Computed once: keccak256("erc8042.facets.X.v1") - 1
bytes32 constant SLOT = 0x9c6f1...4e2;
struct Layout {
mapping(address => uint256) balances;
uint256 totalSupply;
}
function layout() internal pure returns (Layout storage l) {
bytes32 slot = SLOT;
assembly { l.slot := slot }
}
}
contract FacetX {
function balanceOf(address user) external view returns (uint256) {
return FacetXStorage.layout().balances[user];
}
}The slot is deterministic (computed from the facet's name + version) and isolated (no other facet can write to it without explicitly invoking the same library). New facets can be added at runtime via the diamond cut without colliding with existing storage.
Neo Equivalent: Storage Prefix Per Facet
Neo doesn't have Solidity's slot model; it has a flat key-value store addressable by ByteString keys. Storage isolation is achieved by prefixing every facet's storage keys with a facet-unique byte (or hash). The result is the same: each facet owns its key range, no two facets collide.
[DisplayName("FacetX")]
public class FacetX : SmartContract
{
// Single byte prefix per facet — picked at facet design time.
private const byte Prefix_X_Balance = 0x10;
private const byte Prefix_X_TotalSupply = 0x11;
public static BigInteger BalanceOf(UInt160 user)
=> (BigInteger)(Storage.Get(Storage.CurrentContext, BalanceKey(user)) ?? ByteString.Empty);
private static byte[] BalanceKey(UInt160 user)
=> new byte[] { Prefix_X_Balance }.Concat(user);
}| ERC-8042 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
bytes32 SLOT = keccak256("erc8042.facets.X.v1") - 1 | Single byte prefix (e.g. 0x10) per facet | Neo's storage is by-key, not by-slot |
assembly { l.slot := slot } | Direct Storage.Get(ctx, prefixedKey) | No assembly required |
| Diamond cut adds new facet → new slot | Diamond cut adds new facet → new prefix | Same isolation guarantee |
| Storage struct layout encoded by Solidity | Storage layout encoded by your serialisation choice | Use StdLib.Serialize for structs |
| Facet versioning by changing the slot string | Facet versioning by changing the prefix | Same effect |
Why Neo Already Does This
Neo's idiomatic pattern is one prefix byte per state slot (see ERC-7201 mirror for the general case). The diamond specialisation just allocates one prefix per facet. Because Neo's Storage.Find is prefix-scanned and Storage.Get is exact-match, two facets with different prefixes can never collide — the runtime enforces isolation at the storage layer.
The only diamond-specific concern is prefix coordination across facets: when adding a new facet via NEP-22 update, ensure its prefix doesn't collide with an existing one. The pattern below uses a 1-byte registry stored at a known meta-prefix.
// Meta-storage at prefix 0xff: maps facetName → assigned prefix byte.
public static byte AllocatePrefix(string facetName)
{
var ctx = Storage.CurrentContext;
var key = new byte[] { 0xff }.Concat((ByteString)facetName);
var existing = Storage.Get(ctx, key);
if (existing is not null) return ((byte[])existing)[0];
// Find next free byte (skip reserved 0x00, 0xff).
for (byte b = 0x01; b < 0xff; b++)
{
if (Storage.Find(ctx, new byte[] { b }, FindOptions.KeysOnly).Next() is null)
{
Storage.Put(ctx, key, new byte[] { b });
return b;
}
}
throw new Exception("no free prefix");
}Composition
- ERC-2535 — diamond proxy. ERC-8042 is the storage half; pair the two for a complete diamond setup.
- ERC-7201 — namespaced storage layout (broader). Use for non-diamond contracts that still want collision-resistant slots.
- ERC-1167 + ERC-3448 — minimal proxies. Diamond storage doesn't apply (single-implementation proxies) but the same prefix discipline keeps the cloned contract's state organised.
Migration Notes
If you're porting an ERC-2535 + ERC-8042 diamond from Ethereum:
- Each facet's
library FacetXStorage→ a dedicated prefix byte plus a per-key derivation function in the facet's C# code. assembly { l.slot := slot }→Storage.Get(ctx, FacetKey(...))with the facet's prefix.bytes32 SLOT = keccak256(...)→ a single-byte constant or, for contracts with many facets, a hashed prefix (Sha256(facetName)[0..4]) to avoid manual collision tracking.- Diamond cut for a new facet → NEP-22 contract update plus a one-shot call to
AllocatePrefix(newFacetName)if you're using the registry pattern above.
