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.
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:
t_uint256 are Solidity-internal naming. As of this writing, no other EVM language emits the storage layout descriptors in a compatible format. 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:
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.
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. |
| 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).
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. |
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. |
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.
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.
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.
Storage can involve complex nesting of mappings, arrays, and structs. Consumers resolve nested types recursively:
mapping(address => mapping(uint256 => uint256)) uses {key[0]} for the outer key and {key[1]} for the inner key.uint256[][] uses {index[0]} for the outer dimension and {index[1]} for the inner.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.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. |
The label string may embed placeholder tokens that are resolved at display time:
{key[N]} — the Nth mapping key (outermost = {key[0]}), formatted by keyFormats[N] if provided.{index[N]} — the Nth array element index (outermost = {index[0]}), formatted by keyFormats[N] if provided.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.
template is either a plain string or an ordered array of conditional entries:
value string and an optional condition. The wallet evaluates entries in order and uses the first entry whose condition is satisfied. An entry without condition matches unconditionally and SHOULD appear last as the fallback.Template strings support the following placeholders:
{label} — the resolved label of this entry.{oldValue} — the storage value before the transaction, after applying format/params.{newValue} — the storage value after the transaction, after applying format/params.Conditions are JSON Logic expressions as defined in JSON Logic.
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".
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.
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.
This ERC defines a new file format and introduces no backwards compatibility concerns.
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 and related rights waived via CC0.