`0 ** (-1)` is silently evaluated to `0` in a constant expression instead of being rejected as division by zero
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
## Description
`ConstantEvaluator`'s shortcut for exponentiation returns the base unchanged when the base is `0`
or `1`, without checking the sign of the exponent. For base `1` that is correct (`1 ** -1 == 1`),
and the neighbouring `-1` case is handled correctly too. For base `0` it is not: `0 ** -1` is
`1 / 0`, which is undefined, and the compiler quietly produces `0`.
The general path below the shortcut would have caught it — for a negative exponent it returns
`makeRational(denominator, numerator)`, which for base `0` is a rational with a zero denominator.
## Environment
- Compiler version: 0.8.36 (`develop`'s source is identical at these lines)
- Both pipelines (constant evaluation precedes codegen)
## Reproducer
```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract C {
uint256 constant X = 0 ** (-1);
function f() external pure returns (uint256) { return X; }
}
```
**Expected:** rejected, the same way every other constant division by zero is.
**Actual:** compiles cleanly, no error and no warning, and `X` is `0`. The runtime bytecode is
byte-identical to the same contract written with `uint256 constant X = 0;`:
```
solc --bin-runtime --metadata-hash none zero-neg-exp.sol # identical output to `X = 0`
```
(`--metadata-hash none` only removes the trailing CBOR blob, which differs because the source text
differs; the code itself is the same.)
## Controls
All three of these behave correctly, which is what isolates the defect to base `0`:
```solidity
uint256 constant X = 1 / 0; // Error: Built-in binary operator / cannot be applied
// to types int_const 1 and int_const 0.
uint256 constant Y = 2 ** (-1); // rejected -- rational 1/2, not convertible to uint256
int256 constant Z = (-1) ** (-1); // == -1, correct
uint256 constant W = 1 ** (-1); // == 1, correct
```
`Z` and `W` were checked the same way and each compiles to bytecode identical to its literal, so the
two sign-independent bases really are handled correctly. The sign of the exponent is respected
everywhere except for base `0`.
## Cause
`libsolidity/analysis/ConstantEvaluator.cpp:117-128`:
```cpp
case Token::Exp:
{
if (_right.denominator() != 1)
return std::nullopt;
bigint const& exp = _right.numerator();
// x ** 0 = 1
// for 0, 1 and -1 the size of the exponent doesn't have to be restricted
if (exp == 0)
return 1;
else if (_left == 0 || _left == 1) // <-- no sign check on exp
return _left;
else if (_left == -1)
...
```
The comment explains why `0`, `1` and `-1` skip the precision guard — for those bases the
*magnitude* of the exponent does not matter. That reasoning is about size only; the *sign* was
carried along with it by accident. `1` and `-1` are genuinely sign-independent, `0` is not.
## Relationship to #16690
[#16690](https://github.com/argotorg/solidity/issues/16690) (closed) also reports invalid constant expressions being accepted,
but through a different mechanism: operands wrapped in an explicit cast
bypass the compile-time arithmetic checks. This case needs no cast — the literal expression
`0 ** (-1)` reaches the evaluator directly and the evaluator itself returns a value for it.
Contributor guide
Research direction
Start at libsolidity/analysis/ConstantEvaluator.cpp:117-128 and trace the Token::Exp shortcut, then compile the provided 0 ** (-1) reproducer with solc. Done means the constant expression is rejected as division by zero while the listed control expressions retain their current behavior.
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
- 86/100