argotorg / argotorg/solidity

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

Open
#16,944 1 comment 0 reactions 1 assignee Claimed by @matheusaaguiar View on GitHub
bug :bug:
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.