argotorg / argotorg/solidity

Warning 3495 under-counts free slots ignoring packing

Open
#16,820 1 comment 0 reactions 1 assignee Claimed by @nikola-matic View on GitHub
bug :bug: low effort low impact must have eventually
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
2d 19h
Merged PRs (30d)
29

Description

## Description

`warnStorageLayoutBaseNearStorageEnd` at `libsolidity/analysis/PostTypeContractLevelChecker.cpp:230-260` emits Warning 3495 ("This contract is very close to the end of storage.") with a secondary note: "There are N storage slots between this state variable and the end of storage." The value N is computed via `contractStorageSizeUpperBound`, which sums one slot per state variable regardless of packing. For a contract with packed types (bools, uint8s, etc.), the diagnostic under-counts the actually-free slots by up to the packing density (32× for bools).

Concretely, 32 packed bools at `layout at 2**256 - 33` reports "There are 0 storage slots between..." but slots `2**256-32` through `2**256-1` (31 slots) are ALL free.

Operational consequence:
1. A user reads "0 slots between" and concludes the contract is at the absolute upper boundary of storage. In reality 31 slots are free — a structurally different upgrade landscape.
2. Tooling that parses the warning text (linters, deploy automation) and aborts when "0 slots between" appears will spuriously block deploys of packing-dense contracts that have plenty of headroom.
3. The same packing-blind `contractStorageSizeUpperBound` also produces Error 5015 false-positives in a sibling report; one fix (replace `contractStorageSizeUpperBound` with a packing-aware allocator such as `StorageOffsets::computeOffsets`) would resolve both the warning under-count and the error false-positive.

### Source-level evidence

`warnStorageLayoutBaseNearStorageEnd` (`libsolidity/analysis/PostTypeContractLevelChecker.cpp:230-260`):

```cpp
void PostTypeContractLevelChecker::warnStorageLayoutBaseNearStorageEnd(ContractDefinition const& _contract)
{
if (Error::containsErrors(m_errorReporter.errors())) return;

bigint storageSize = contractStorageSizeUpperBound(_contract, VariableDeclaration::Location::Unspecified);
u256 baseSlot = layoutBaseForInheritanceHierarchy(_contract, DataLocation::Storage);
solAssert(baseSlot + storageSize <= std::numeric_limits::max());

if (
u256 slotsLeft = std::numeric_limits::max() - baseSlot - u256(storageSize);
slotsLeft <= u256(1) << 64
)
{
...
m_errorReporter.warning(
errorID,
location,
"This contract is very close to the end of storage. This limits its future upgradability.",
SecondarySourceLocation{}.append(
"There are " + slotsLeft.str() + " storage slots between this state variable and the end of storage.",
lastStorageVariable->location()
)
);
}
}
```

`contractStorageSizeUpperBound` (`libsolidity/ast/ASTUtils.cpp:111-124`):

```cpp
bigint contractStorageSizeUpperBound(ContractDefinition const& _contract, VariableDeclaration::Location _location)
{
bigint size = 0;
for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts)
for (VariableDeclaration const* variable: contract->stateVariables())
if (!(variable->isConstant() || variable->immutable()) && variable->referenceLocation() == _location)
size += variable->annotation().type->storageSizeUpperBound();
return size;
}
```

`storageSizeUpperBound()` on value types returns 1 per variable, so 32 bools → upperBound 32, while actual storage = 1 slot. `slotsLeft = max - baseSlot - upperBound` therefore reports `0` when the true number of free slots is 31.

Expected: the secondary note's count should reflect the packing-aware actual storage allocation (i.e. the number of slots truly between the contract's last used storage slot and `2**256 - 1`), not a per-variable upper bound.

## Environment

- Compiler version: 0.8.35-develop.2026.5.5+commit.47b9dedd.Linux.g++
- Operating system: Linux Ubuntu Jammy

## Steps to Reproduce

Place a contract with 32 packed `bool` state variables at the very top of the storage region using `layout at`:

```solidity
// 32 packed bools at the very top of storage.
// Actual storage: 1 slot at 2**256-33, with 31 slots free above it.
// Warning text claims: "0 slots between" — under-count of 31.
contract C layout at 2**256 - 33 {
bool a; bool b; /* ... */ bool ff; // 32 variables, all packed into 1 slot
}
```

Compile with `solc`. Observed diagnostic:

```
Warning (3495): This contract is very close to the end of storage. This limits its future upgradability.
Note: There are 0 storage slots between this state variable and the end of storage.
--> points at `bool ff;` (last variable)
```

Reality (verified by a forge test, 2 passing):
- All 32 bools pack into slot `2**256-33` (every byte is `0x01` after `setAll()`).
- Slots `2**256-32` .. `2**256-1` are all zero — 31 slots are free above the last used slot.

Verification command used:

```
$ SOLC_PATH=/workspace/solidity/build/solc/solc \
bash research/scripts/verify_compile_error.sh \
research/compile_tests/E56ab83_warning_3495_undercounts_packed_slots.input.sol \
research/compile_tests/E56ab83_warning_3495_undercounts_packed_slots.expect
OK: solc emitted codeless error message containing 'There are 0 storage slots between' for ...
```

The over-count ratio is the packing density (up to 32× for bool/uint8); other packing combinations produce smaller but still observable under-counts in the warning's "N storage slots between..." text.

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.