EIP-8317 - Contract Storage Layout Descriptor Format

Created 2026-06-25
Status Draft
Category Interface
Type Standards Track
Authors
Requires

Abstract

Definition of a JSON format for describing the "storage layout" of an EVM contract.

The storage layout represents the mapping from the smart contracts' state variables as defined in the source code to their EVM-level storage locations.

The format is extensible and is therefore not tied to any single source language or compiler. It can accommodate layouts produced by any EVM-targeting compiler, as well as more advanced and manually controlled use-cases like ERC-7201, ERC-8042, and hand-written EVM assembly storage.

Consumers of these descriptors — block explorers, debuggers, source-verification services, security tooling, and wallets performing clear signing, transaction simulations, and transaction assertions — can use a single, predictable schema regardless of how a contract was written.

Motivation

Tools that need to make sense of raw storage slots, such as block explorers, debuggers, wallets that show users the outcome of a transaction before they sign, and other similar projects, currently have no common and trusted layout description format and have to rely on their own custom storage layout interpretation.

Solidity's storageLayout JSON output often serves as a de facto standard, but it has some structural problems making it unfit to be used in scenarios where a finalized ERC-level standard would be required:

  1. It is Solidity specific. For example, type identifiers such as t_uint256 are Solidity-internal naming. As of this writing, no other EVM language emits the storage layout descriptors in a compatible format.
  2. It is not stable. The format documentation states the storage layout is experimental and the format is subject to change in non-breaking releases.
  3. It does not support any common alternative layouts. For example, namespaced storage (ERC-7201) and diamond storage (ERC-8042) place structs at a keccak256-derived slot reached through an inline-assembly pointer. Solidity compiler is not aware of these standards and is unable to generate their layout's descriptor.

The proposed storage layout format is intended for cross-chain, cross-tool, and cross-language wallets that need to translate raw (contract, slot, old_value, new_value) tuples from transaction simulation or EIP-7906 transaction assertions into a meaningful description of which variable changed as the result of this transaction.

Therefore, this format must satisfy the following requirements:

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

File structure

A storage layout file is a JSON object with the following top-level members. See storage-layout.schema.json for the full JSON Schema, example-erc20.json for an ordinary sequential layout, example-eip7702-namespaced.json for a layout mixing sequential and ERC-7201 namespaced storage, and example-erc8042-diamond.json for an ERC-8042 Diamond Storage layout.

Field Required Description
$schema No URI of the JSON Schema this file conforms to.
$comment No Free-text documentation of the file's purpose or provenance.
compiler Yes Object identifying the source language and compiler used. See Compiler object.
storage Yes Ordered array of top-level storage entries.
types Yes Object mapping type identifiers to type definitions.

Compiler object

Field Required Description
language Yes Source language. This ERC defines support for "Solidity", "Vyper", and "manual".
version No Compiler version string (e.g. "0.8.24").
settings No Arbitrary compiler settings object.

A single file MAY mix entries whose slots were assigned by the declared compiler with entries that were defined manually.

"manual" MUST be used as language when no compiler produced any part of the layout (e.g. Huff, raw Yul, hand-written assembly).

Storage entry

Each element of the top-level storage array, and each element of a struct type's members array, is a storage entry:

Field Required Description
label Yes Source-level variable name.
slot Yes Decimal string of the storage slot where this entry resides or starts.
offset Yes Byte offset within the slot where this entry begins. 0 for any type occupying a full slot or more.
type Yes Identifier referencing an entry in the file's types object.
path No Stable dot-separated identifier for this entry (e.g. "vaults.lockedAmount"). If absent, consumers MUST compute it by joining label values from the top-level entry down through struct members.
source No "compiler" (default) or "manual". "compiler" means the slot and offset were emitted by the declared compiler from verifiable source. "manual" means the author declared them directly.
baseSlotDerivation No For entries whose base slot was computed via an alternative scheme rather than simple sequential compiler assignment. See Base Slot Derivation.
display No Optional display annotations for wallets. See Display Annotations.

Type definition

Each value in the top-level types object describes one type referenced by type fields elsewhere in the file:

Field Required Description
encoding Yes Storage encoding: inplace, inplace_struct, mapping, dynamic_array, or bytes. See Slot Derivation Formulas.
label Yes Human-readable type name as it appears in the source language (e.g. "uint256", "mapping(address => uint256)").
numberOfBytes Yes Decimal string: number of bytes this type occupies. Used with the entry's offset to extract a packed value.
key mapping only Type identifier of the mapping key.
value mapping and dynamic_array only Type identifier of the mapping value or array element.
base dynamic_array only Alias of value, kept for literal compatibility with Solidity compiler output.
members inplace_struct only Ordered array of storage entries describing the struct's members.

Base Slot Derivation

An entry's baseSlotDerivation object names the scheme used to compute its slot, so that consumers can independently recompute and verify it:

Field Required Description
scheme Yes One of supported base slot calculation algorithms. Currently "erc7201", "erc8042", or "custom".
namespaceId erc7201, erc8042 only The namespace id string the scheme hashes.
formula custom only Free-text, non-machine-verified description of how slot was computed.

For scheme: "erc7201", consumers MAY verify slot == keccak256(keccak256(namespaceId) - 1) & ~0xff per ERC-7201.

For scheme: "erc8042", consumers MAY verify slot == keccak256(namespaceId) per ERC-8042.

"custom" exists for schemes not yet standardized by an ERC; its slot is hand-authored.

baseSlotDerivation MUST NOT be used for sequential, compiler-assigned slots, including slots shifted by Solidity's native layout at base-slot syntax — in both cases the compiler emits the final, absolute slot directly, and no separate re-derivation is meaningful.

Packed Variables

Solidity and Vyper may pack multiple small variables into a single 32-byte slot. When a packed slot is modified, consumers MUST identify which variables changed by inspecting each entry whose slot matches, using offset and numberOfBytes from the entry's type to extract the specific bytes.

To extract the value of a packed variable from a raw 32-byte slot word: read numberOfBytes bytes starting at byte offset within the slot.

When a packed slot changes, wallets MAY omit variables whose extracted old and new values are identical.

Slot Derivation Formulas

inplace types (primitives and fixed-size value types such as uint256, address, bytes32) are stored directly at their declared slot and offset; no further derivation is needed.

Complex types are not stored directly at their entry's slot; child values are stored at a position computed from the entry's slot (acting as a base slot), the type's encoding, and the declared compiler.language. Wallets and other consumers that need to map a raw modified slot back to a storage entry MUST implement the formulas below for every compiler.language value they support:

Language encoding Derivation formula
Solidity inplace_struct baseSlot + member.slot
Solidity mapping keccak256(abi.encode(key, baseSlot))
Solidity dynamic_array keccak256(baseSlot) + index * elementSlots
Solidity bytes Short form (numberOfBytes(value) <= 31): inline in baseSlot. Long form: data at keccak256(baseSlot), sequential. Length is stored in the low-order byte of baseSlot.
Vyper inplace_struct baseSlot + member.slot
Vyper mapping keccak256(abi.encode(key, baseSlot)) (Vyper ≥ 0.3.0) or keccak256(abi.encode(baseSlot, key)) (Vyper < 0.3.0) — note the reversed key/slot order in legacy Vyper.
Vyper dynamic_array baseSlot + 1 + index * elementSlots
Vyper bytes Stored inline starting at baseSlot, spanning ceil(length / 32) slots sequentially. Unlike Solidity, Vyper never relocates bytes/string data behind a keccak256 indirection.

Where elementSlots = ceil(elementSize / 32), derived from the element type's numberOfBytes. For nested types, derivation is applied recursively at each level.

This table is the canonical, normative source of derivation formulas defined by this ERC. Support for additional values of compiler.language MUST be added via an update to this ERC.

Nested and Complex Types

Storage can involve complex nesting of mappings, arrays, and structs. Consumers resolve nested types recursively:

  1. Mappings: Each nesting level adds a key. mapping(address => mapping(uint256 => uint256)) uses {key[0]} for the outer key and {key[1]} for the inner key.
  2. Arrays: Each nesting level adds an index. uint256[][] uses {index[0]} for the outer dimension and {index[1]} for the inner.
  3. Structs: Members are defined in the types object (with encoding: "inplace_struct"), each with their own optional display annotation. When a struct member changes, the wallet displays the parent variable's resolved label alongside the member's own label.

Display Annotations

Each storage entry and each struct member in types may carry an optional display object. These annotations follow the ERC-7730 display vocabulary and are consumed by wallets to present storage changes in human-readable form during transaction simulation and clear signing.

Field Required Description
label No User-facing name for the variable. Supports {key[N]} and {index[N]} placeholders. See Labels and Placeholders.
description No Human-readable explanation of the variable's semantic meaning. Informative only — MUST NOT be relied upon for security decisions without additional authenticity assurances.
keyFormats No Array of formatters applied to mapping keys or array indices before they are embedded in label. See Key Formats.
format No The ERC-7730 formatter (tokenAmount, addressName, raw, date, etc.) applied to the storage value itself. Same vocabulary as display.formats[*].fields[*].format in ERC-7730.
params No Formatter parameters for format. Same structure as display.formats[*].fields[*].params in ERC-7730.
template No Natural-language description of the change to this variable. See Templates and JSON Logic.

Labels and Placeholders

The label string may embed placeholder tokens that are resolved at display time:

Key Formats

keyFormats is an ordered array of formatter objects. Each entry has the same format and params structure as ERC-7730 field formatters and is applied to the corresponding key or index level before it is embedded in label.

keyFormats[0] applies to {key[0]} or {index[0]}, keyFormats[1] to {key[1]} or {index[1]}, and so on.

Templates and JSON Logic

template is either a plain string or an ordered array of conditional entries:

Template strings support the following placeholders:

Conditions are JSON Logic expressions as defined in JSON Logic.

Display Examples

ERC-20 balances mapping:

{
    "label": "_balances",
    "slot": "1",
    "type": "t_mapping(t_address,t_uint256)",
    "display": {
        "label": "Balance of {key[0]}",
        "description": "Token balance held by each account.",
        "keyFormats": [{ "format": "addressName" }],
        "format": "tokenAmount",
        "params": { "tokenPath": "@.to" },
        "template": [
            {
                "condition": { ">": [{ "var": "newValue" }, { "var": "oldValue" }] },
                "value": "Balance of {key[0]} increased from {oldValue} to {newValue}"
            },
            {
                "condition": { "<": [{ "var": "newValue" }, { "var": "oldValue" }] },
                "value": "Balance of {key[0]} decreased from {oldValue} to {newValue}"
            },
            {
                "value": "Balance of {key[0]} changed from {oldValue} to {newValue}"
            }
        ]
    }
}

Mapping to a struct (vault pattern):

struct VaultInfo {
    uint256 lockedAmount;
}

mapping(address => VaultInfo) private vaults;
{
    "storage": [{
        "label": "vaults",
        "slot": "10",
        "type": "t_mapping(t_address,t_struct(VaultInfo))",
        "display": {
            "label": "Vault belonging to {key[0]}",
            "keyFormats": [{ "format": "addressName" }]
        }
    }],
    "types": {
        "t_struct(VaultInfo)": {
            "encoding": "inplace_struct",
            "label": "struct VaultInfo",
            "numberOfBytes": "32",
            "members": [
                {
                    "label": "lockedAmount",
                    "slot": "0",
                    "offset": 0,
                    "type": "t_uint256",
                    "display": {
                        "label": "Locked Amount",
                        "format": "tokenAmount",
                        "params": { "tokenPath": "@.to" },
                        "template": [
                            {
                                "condition": { ">": [{ "var": "newValue" }, { "var": "oldValue" }] },
                                "value": "Locked Amount increased from {oldValue} to {newValue}"
                            },
                            {
                                "condition": { "<": [{ "var": "newValue" }, { "var": "oldValue" }] },
                                "value": "Locked Amount decreased from {oldValue} to {newValue}"
                            }
                        ]
                    }
                }
            ]
        }
    }
}

When vaults[0xAlice].lockedAmount changes, the wallet presents the parent label "Vault belonging to Alice.eth" alongside the member label "Locked Amount".

JSON Logic

Conditions are JSON Logic expressions. This ERC normatively defines the subset of JSON Logic used by conditions and does not depend on the external JSON Logic specification.

A JSON Logic expression is a JSON object with exactly one key naming the operator and an array of operands as its value:

{ "<operator>": [<operand>, <operand>, ...] }

Operands are themselves JSON Logic expressions or literal JSON values. A condition passes when its top-level expression evaluates to a truthy value.

Expressions are evaluated against the following context:

Variable Type Description
oldValue value Storage value before the transaction, after applying format.
newValue value Storage value after the transaction, after applying format.
key array Raw mapping keys at each nesting level, outermost first.
index array Array element indices at each nesting level, outermost first.

The var operator returns the value of a context variable: {"var": "oldValue"} evaluates to the oldValue variable. Elements of key and index are addressed with dot-separated paths: {"var": "key.0"} returns the outermost mapping key, {"var": "index.1"} returns the second array index, and so on.

The following operators MUST be supported:

Operator Example Returns
== {"==": [a, b]} true if a equals b.
!= {"!=": [a, b]} true if a does not equal b.
> {">": [a, b]} true if a is greater than b.
>= {">=": [a, b]} true if a is greater than or equal to b.
< {"<": [a, b]} true if a is less than b.
<= {"<=": [a, b]} true if a is less than or equal to b.
! {"!": [a]} true if a is falsy.
and {"and": [a, b]} true if both a and b are truthy.
or {"or": [a, b]} true if either a or b is truthy.
var {"var": "<path>"} Value of the context variable at <path>.

The comparison operators accept two operands and return a boolean. ! accepts a single operand. and and or accept one or more operands. var accepts a single string operand. The values false, 0, the empty string, the empty array, and null are falsy; all other values are truthy.

Wallets MUST NOT rely on any other JSON Logic operators or features being available.

Rationale

Transient storage is out of scope

While any change persisted in the contract's storage may be extremely consequential for the user, the transient storage is cleared after the transaction completes and bears no effect on the transaction outcome.

There is no use-case that would require knowledge of the transient storage variable layout in the same way as the persistent one.

Backwards Compatibility

This ERC defines a new file format and introduces no backwards compatibility concerns.

Security Considerations

A storage layout file that misrepresents a slot's type or path could cause a consumer to display a misleading variable name or formatted value for a real storage change. This ERC does not define a trust or distribution mechanism for storage layout files. Standards that reference files conforming to this ERC, such as ERC-7730, are responsible for defining how a layout file's authenticity is established before it is relied upon for any security-relevant display.

Copyright

Copyright and related rights waived via CC0.