User-Defined Storage Layout Policies for Dynamic Arrays
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
## Abstract
Solidity hardcodes how dynamic arrays map their length and data to storage slots: length lives at the declared slot, and data starts at `keccak256(slot)`. This makes it impossible for developers to control storage locality for array operations.
On chains with page-aligned storage models — where accessing slots within the same page is significantly cheaper than crossing page boundaries — this fixed layout forces every push, pop, or indexed access to touch at least two cold pages (one for the length slot, one for the data area at a keccak hash far away). For contracts with hot-path array operations (order books, token registries, AMM tick arrays), this is a meaningful and unavoidable gas penalty.
There is currently no way in Solidity to override how array.push(), array.pop(), array.length, or array[i] compute their storage slots. Libraries can wrap these operations, but they cannot change the codegen for native array syntax.
## Motivation
Monad's [MIP-8](https://github.com/monad-crypto/MIPs/blob/main/MIPS/MIP-8.md) partitions EVM storage into 4096-byte pages (128 slots). Once a page is loaded, subsequent reads/writes to slots within the same page are warm. While Monad's MIP-8 is the immediate motivation, any future EVM-compatible chain or EIP that introduces locality-sensitive storage pricing would benefit from the same mechanism. The feature is chain-agnostic — the developer chooses the layout policy via Yul assembly, so it adapts to any storage model without compiler changes.
A developer should be able to write:
```solidity
storage_layout pageAligned(uint256 slot) -> (uint256 lenSlot, uint256 dataSlot) {
assembly {
mstore(0, slot)
lenSlot := and(keccak256(0, 0x20), not(0x7f)) // align to 128-slot page
dataSlot := add(lenSlot, 1)
}
}
contract A {
uint256[] pageAligned array;
}
```
With this layout, prices.push(42) reads the length and writes the new element on the same 128-slot page — a single cold page load instead of two. On a page-aligned chain, this can cut storage costs for array-heavy contracts.
The existing `layout at` specifier only controls the base slot offset for an entire contract. It cannot change how individual arrays compute their data area.
## Specification
New declaration: `storage_layout`
```
storage_layout ( ) -> ( , ) {
assembly { ... }
}
```
Can be declared at file level or inside a contract body.
The body is a Yul assembly block that receives the array's declared storage slot and must compute two values: the slot where the array length is stored (`lenSlot`) and the slot where element data begins (`dataSlot`).
The parameter type must be `uint256`. The return types must both be `uint256`.
### Type annotation
A layout is applied to a dynamic array type by placing the layout name between [] and the variable name:
```solidity
uint256[] pageAligned prices;
uint256[] pageAligned public volumes;
```
The layout becomes part of the type system. `uint256[] pageAligned` and `uint256[]` are distinct types with different `richIdentifier()` values. This means:
- Storage pointers carry the layout: `function update(uint256[] pageAligned storage arr) internal`
- Implicit conversion between layout and non-layout arrays is rejected at compile time.
- Multiple layout policies can coexist in the same contract.
- The codegen function cache is keyed by type identifier, so layout-aware and standard functions are generated separately without conflicts.
### Restrictions
- Only dynamic arrays (T[]) are supported
- bytes and string are not supported because their short/long encoding scheme is incompatible with user-defined slot computation.
- The Yul body is trusted — the compiler does not verify that lenSlot and dataSlot don't collide with other state variables. This is the developer's responsibility, same as with inline assembly.
## Backwards Compatibility
This feature is fully backwards compatible:
- No change to existing bytecode. The layout check in codegen can a null pointer comparison (`storageLayout() != nullptr`). Contracts without layout annotations generate identical bytecode.
- No change to existing ABI. The storage layout is a codegen concern and does not affect the contract's external interface.
- JSON AST changes are additive. `ArrayTypeName` gains an optional `storageLayoutName` field (absent when no layout is used). `StorageLayoutDefinition` is a new node type that only appears when the feature is used
Contributor guide
Assessment
This issue has not been assessed yet.