storageLayout: `numberOfBytes` is computed in `u256` and wraps for types spanning 2^251 or more slots
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
## Description
`libsolidity/interface/StorageLayout.cpp:74` computes the `numberOfBytes` field of the
`storageLayout` output artifact with an unchecked 256-bit multiplication:
```cpp
typeInfo["numberOfBytes"] = u256(_type->storageBytes() * _type->storageSize()).str();
```
`storageSize()` is a `u256` slot count and `storageBytes()` is 32, so the product wraps silently
and the emitted value is `(32 * slots) mod 2^256`.
The existing guard does not cover this because it is denominated in a different unit.
`ContractLevelChecker::checkStorageSize` bounds the **slot** count below 2^256, whereas
`numberOfBytes` requires `32 * slots < 2^256`, i.e. `slots < 2^251`. Every size in
`[2^251, 2^256)` is accepted, compiles, and wraps.
For contrast, `ArrayType::storageSize()` (`Types.cpp:1830-1831`) deliberately promotes to `bigint`
and asserts the slot count fits in 256 bits, and `StorageOffsets::computeOffsets`
(`Types.cpp:142-172`) uses `bigint` with two guards. `StorageLayout.cpp:74` appears to be the only
place in `libsolidity`/`libyul` that multiplies `storageBytes()` by `storageSize()`, which is
presumably why codegen is unaffected.
**Expected:** `numberOfBytes` greater than 32, consistent with the `slot` values in the same
document. `docs/internals/layout_in_storage.rst:312-313` states: *"`numberOfBytes` is the number of
used bytes (as a decimal string). Note that if `numberOfBytes > 32` this means that more than one
slot is used."*
**Actual:** `numberOfBytes` is `32` — byte-for-byte identical to a plain `uint256` — while the same
JSON places the following variable at slot 2^251+1. The document contradicts itself, because the
`slot` values come from the overflow-checked `bigint` path and only the byte count is wrong.
Note this is an output-artifact defect only: slots, offsets and generated code are all correct.
## Environment
- Compiler version: 0.8.36+commit.8a079791 (also reproduced on 0.8.0 through 0.8.36; source is
byte-identical on current `develop`)
- Compilation pipeline (legacy, IR, SSA CFG): all — the defect is in output generation, not codegen
- Target EVM version (as per compiler settings): default (`osaka`); not EVM-version dependent
- Framework/IDE (e.g. Foundry, Hardhat, Remix): `solc` command line directly
- EVM execution environment / backend / blockchain client: n/a — no execution required
- Operating system: macOS 26.5.2
## Steps to Reproduce
`poc.sol`:
```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
uint256[2**251 + 1] v;
uint256 tail;
}
```
```
solc --storage-layout poc.sol
```
Relevant output:
```
v slot=0 numberOfBytes=32
tail slot=3618502788666131106986593281521497120414687020801267626233049500247285301249
```
`v` reports 32 bytes while `tail` is placed 2^251+1 slots later.
**Control** — a genuinely one-slot variable produces the identical `numberOfBytes`:
```solidity
contract C { uint256 v; uint256 tail; }
```
```
v slot=0 numberOfBytes=32
tail slot=1 numberOfBytes=32
```
The reported value follows `(32 * slots) mod 2^256` exactly. Checked at eight points, the first
three being correct-value controls:
| declaration | slots | reported | correct value |
|---|---|---|---|
| `uint256 v` | 1 | 32 | 32 |
| `uint256[2**250]` | 2^250 | 2^255 | 2^255 |
| `uint256[2**250 + 1]` | 2^250+1 | 2^255+32 | 2^255+32 |
| `uint256[2**251]` | 2^251 | **0** | 2^256 |
| `uint256[2**251 + 1]` | 2^251+1 | **32** | 2^256+32 |
| `uint256[2**251 + 2]` | 2^251+2 | **64** | 2^256+64 |
| `uint256[2**126][2**126]` | 2^252 | **0** | 2^257 |
| `uint256[2**255]` | 2^255 | **0** | 2^260 |
Struct wrappers propagate it: `struct Big { uint256[2**251 + 1] arr; }` reports
`numberOfBytes: "32"` for `Big`.
`--transient-storage-layout` cannot reach this, since reference types are rejected in transient
storage.
Contributor guide
Assessment
This issue has not been assessed yet.