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
// 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:
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:
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 (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.
Description
ConstantEvaluator's shortcut for exponentiation returns the base unchanged when the base is0or
1, without checking the sign of the exponent. For base1that is correct (1 ** -1 == 1),and the neighbouring
-1case is handled correctly too. For base0it is not:0 ** -1is1 / 0, which is undefined, and the compiler quietly produces0.The general path below the shortcut would have caught it — for a negative exponent it returns
makeRational(denominator, numerator), which for base0is a rational with a zero denominator.Environment
develop's source is identical at these lines)Reproducer
Expected: rejected, the same way every other constant division by zero is.
Actual: compiles cleanly, no error and no warning, and
Xis0. The runtime bytecode isbyte-identical to the same contract written with
uint256 constant X = 0;:(
--metadata-hash noneonly removes the trailing CBOR blob, which differs because the source textdiffers; the code itself is the same.)
Controls
All three of these behave correctly, which is what isolates the defect to base
0:ZandWwere checked the same way and each compiles to bytecode identical to its literal, so thetwo 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:The comment explains why
0,1and-1skip the precision guard — for those bases themagnitude of the exponent does not matter. That reasoning is about size only; the sign was
carried along with it by accident.
1and-1are genuinely sign-independent,0is not.Relationship to #16690
#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.