#16944·solidity

storageLayout: `numberOfBytes` is computed in `u256` and wraps for types spanning 2^251 or more slots

Author: Lokkw0510Created Aug 26, 2026Updated Sep 12, 2026
Labelsbug :bug:

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.