Skip to content

0 ** (-1) is silently evaluated to 0 in a constant expression instead of being rejected as division by zero #16982

Description

@Lokkw0510

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bug 🐛low impactChanges are not very noticeable or potential benefits are limited.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions