`fitsPrecisionExp` charges the base's bit *length* per multiplication instead of its bit *position*, so valid constant powers such as `2 ** 2049` are rejected
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 29
Description
## Description
The precision guard for constant exponentiation computes the width of `base ** exp` as
`exp * (msb(base) + 1)`. `msb()` is a zero-based bit *position*, so `msb + 1` is the bit *length*
of the base, and multiplying that whole length by the exponent charges the `+1` adjustment once per
multiplication rather than once overall.
The true width of `base ** exp` is `floor(exp * log2(base)) + 1`, i.e. about `exp * msb + 1`.
The guard therefore over-charges by roughly `exp - 1` bits and rejects constant expressions that fit
comfortably inside the 4096-bit budget.
For base 2 (`msb == 1`) the effect is worst: the guard charges `2 * exp` bits, halving the usable
exponent.
## Environment
- Compiler version: 0.8.36 (`develop`'s source is identical at these lines)
- The guard runs during constant evaluation, so both pipelines are affected
## Reproducer
Accepted:
```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract C { uint256 constant X = (2 ** 2048) / (2 ** 2048); }
```
Rejected — one larger:
```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract C { uint256 constant X = (2 ** 2049) / (2 ** 2049); }
```
```
Error: Built-in binary operator ** cannot be applied to types int_const 2 and int_const 2049.
--> rejected.sol:3:36:
|
3 | contract C { uint256 constant X = (2 ** 2049) / (2 ** 2049); }
| ^^^^^^^^^
```
`2 ** 2049` is a 2050-bit number and the budget is 4096 bits, so it should be accepted.
(The division keeps the *result* a `uint256`; the intermediate is what the guard is judging.)
## Measured boundary
Acceptance tracks the formula exactly rather than the real width. `formula = exp * (msb(base) + 1)`,
`true = floor(exp * log2(base)) + 1`, budget 4096 (solc 0.8.36):
| expression | formula | true width | result |
|---|---|---|---|
| `2 ** 2048` | 4096 | 2049 | accepted |
| `2 ** 2049` | 4098 | 2050 | **rejected** |
| `2 ** 4095` | 8190 | 4096 | **rejected** |
| `2 ** 4096` | 8192 | 4097 | rejected (correct — genuinely over budget) |
| `4 ** 1365` | 4095 | 2731 | accepted |
| `4 ** 1366` | 4098 | 2733 | **rejected** |
| `3 ** 2048` | 4096 | 3247 | accepted |
| `3 ** 2049` | 4098 | 3248 | **rejected** |
Accepted in every row iff `formula <= 4096`, regardless of the true width. For base 2 the whole
exponent range 2049–4095 is wrongly refused.
## Cause
`libsolidity/analysis/ConstantEvaluator.cpp:48-65` (`fitsPrecisionExp`), line 63:
```cpp
std::size_t mostSignificantBaseBit = static_cast(boost::multiprecision::msb(_base));
if (mostSignificantBaseBit == 0) // _base == 1
return true;
if (mostSignificantBaseBit > bitsMax) // _base >= 2 ^ 4096
return false;
bigint bitsNeeded = _exp * (mostSignificantBaseBit + 1); // <-- the +1 is inside the product
return bitsNeeded <= bitsMax;
```
The `+1` belongs outside the multiplication (`_exp * mostSignificantBaseBit + 1`), which is the
standard bound for the bit length of a power. The `mostSignificantBaseBit == 0` early return for
`_base == 1` shows the intent was a per-base adjustment, not a per-multiplication one.
Called from `:140` for the `Token::Exp` case.
Contributor guide
Research direction
Start in libsolidity/analysis/ConstantEvaluator.cpp at fitsPrecisionExp, then inspect its Token::Exp call site around line 140. Compile the supplied 2 ** 2048 and 2 ** 2049 reproducers and compare the measured boundaries; done means powers within the 4096-bit budget are accepted while genuinely oversized powers remain rejected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, solidity
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100