Warning 3495 under-counts free slots ignoring packing
- 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
Assessment
This issue has not been assessed yet.