storageLayout: `numberOfBytes` is computed in `u256` and wraps for types spanning 2^251 or more slots
Description
libsolidity/interface/StorageLayout.cpp:74 computes the numberOfBytes field of the
storageLayout output artifact with an unchecked 256-bit multiplication:
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):
solccommand line directly - EVM execution environment / backend / blockchain client: n/a — no execution required
- Operating system: macOS 26.5.2
Steps to Reproduce
poc.sol:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
uint256[2**251 + 1] v;
uint256 tail;
}solc --storage-layout poc.solRelevant output:
v slot=0 numberOfBytes=32
tail slot=3618502788666131106986593281521497120414687020801267626233049500247285301249v reports 32 bytes while tail is placed 2^251+1 slots later.
Control — a genuinely one-slot variable produces the identical numberOfBytes:
contract C { uint256 v; uint256 tail; }v slot=0 numberOfBytes=32
tail slot=1 numberOfBytes=32The 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.
Source: argotorg/solidity