Repository: vyperlang/vyper

Issue:
no runtime check for raising 1 or 0 to negative number
### Version Information

vyper Version (output of vyper --version): https://github.com/vyperlang/vyper/commit/be2c59a604070c75212dca90beb4c33fed908c8b
Python Version (output of python --version): 3.8.10

### What's your issue about?

the following code should (in order to be consistent with other bases) raise an exception at runtime, but does not
```vyper
@external
def bar() -> int16:
  x: int16 = -2
  return 1 ** x
```

note that this raises InvalidOperation at compile time:
```vyper
@external
def foo() -> int16:
  x: int16 = 1
  return x ** (-2)
```


Discussion:
a commented out test case for this was added in #2983, now just a matter of fixing - https://github.com/vyperlang/vyper/blob/e9c5655db2fde303c980612be5c924f1e5889d3a/tests/parser/types/numbers/test_signed_ints.py#L62-L74

Resolving patch:
diff --git a/vyper/codegen/arithmetic.py b/vyper/codegen/arithmetic.py
--- a/vyper/codegen/arithmetic.py
+++ b/vyper/codegen/arithmetic.py
@@ -32,7 +32,7 @@ def calculate_largest_power(a: int, num_bits: int, is_signed: bool) -> int:
     if num_bits % 8:  # pragma: no cover
         raise CompilerPanic("Type is not a modulo of 8")
 
-    if a in (0, 1):  # pragma: no cover
+    if a in (-1, 0, 1):  # pragma: no cover
         raise CompilerPanic("Exponential operation is useless!")
 
     value_bits = num_bits - (1 if is_signed else 0)
@@ -70,8 +70,9 @@ def calculate_largest_power(a: int, num_bits: int, is_signed: bool) -> int:
         assert num_iterations < 10000
 
     # Edge case: If a is negative and the values of a and b are such that:
-    #               (a) ** (b + 1) == -(2 ** value_bits)
-    #            we can actually squeak one more out of it because it's on the edge
+    #   (-a) ** (b + 1) == -(2 ** value_bits)
+    # we can squeak one more out of it because lower bound of signed ints
+    # is slightly wider than upper bound
     if a_is_negative and (-a) ** (b + 1) == -(2 ** value_bits):  # NOTE: a = abs(a)
         return b + 1
     else:
@@ -345,35 +346,36 @@ def safe_pow(x, y):
         # type checker should have caught this
         raise TypeCheckFailure("non-integer pow")
 
-    if x.is_literal:
-        # cannot pass 1 or 0 to `calculate_largest_power`
-        if x.value == 1:
-            return IRnode.from_list([1])
-        if x.value == 0:
-            return IRnode.from_list(["iszero", y])
+    GE = "sge" if num_info.is_signed else "ge"
 
-        upper_bound = calculate_largest_power(x.value, num_info.bits, num_info.is_signed)
-        # for signed integers, this also prevents negative values
-        ok = ["le", y, upper_bound]
+    if x.is_literal:
+        # cannot pass -1, 0 or 1 to `calculate_largest_power`
+        if x.value in (-1, 0, 1):
+            # not strictly needed, but consistent with other bases
+            # (note: unsigned (ge y 0) will get optimized out)
+            ok = [GE, y, 0]
+        else:
+            upper_bound = calculate_largest_power(x.value, num_info.bits, num_info.is_signed)
+            # for signed integers, this also prevents negative values
+            ok = ["le", y, upper_bound]
 
     elif y.is_literal:
-        # cannot pass 1 or 0 to `calculate_largest_base`
-        if y.value == 1:
-            return x
-        if y.value == 0:
-            return IRnode.from_list([1])
-
-        lower_bound, upper_bound = calculate_largest_base(
-            y.value, num_info.bits, num_info.is_signed
-        )
-        if num_info.is_signed:
-            ok = ["and", ["sge", x, lower_bound], ["sle", x, upper_bound]]
+        # cannot pass 0 or 1 to `calculate_largest_base`
+        if y.value in (0, 1):
+            ok = [1]
         else:
-            ok = ["le", x, upper_bound]
+            lower_bound, upper_bound = calculate_largest_base(
+                y.value, num_info.bits, num_info.is_signed
+            )
+            if num_info.is_signed:
+                ok = ["and", ["sge", x, lower_bound], ["sle", x, upper_bound]]
+            else:
+                ok = ["le", x, upper_bound]
     else:
         # `a ** b` where neither `a` or `b` are known
         # TODO this is currently unreachable, once we implement a way to do it safely
         # remove the check in `vyper/context/types/value/numeric.py`
         return
 
-    return IRnode.from_list(["seq", ["assert", ok], ["exp", x, y]])
+    assertion = IRnode.from_list(["assert", ok], error_msg="safepow")
+    return IRnode.from_list(["seq", assertion, ["exp", x, y]])


Test patch:
diff --git a/tests/parser/types/numbers/test_signed_ints.py b/tests/parser/types/numbers/test_signed_ints.py
--- a/tests/parser/types/numbers/test_signed_ints.py
+++ b/tests/parser/types/numbers/test_signed_ints.py
@@ -16,7 +16,7 @@
 
 
 @pytest.mark.parametrize("typ,lo,hi,bits", PARAMS)
-def test_exponent_base_zero(get_contract, typ, lo, hi, bits):
+def test_exponent_base_zero(get_contract, assert_tx_failed, typ, lo, hi, bits):
     code = f"""
 @external
 def foo(x: {typ}) -> {typ}:
@@ -25,14 +25,14 @@ def foo(x: {typ}) -> {typ}:
     c = get_contract(code)
     assert c.foo(0) == 1
     assert c.foo(1) == 0
-    assert c.foo(-1) == 0
-
-    assert c.foo(lo) == 0
     assert c.foo(hi) == 0
 
+    assert_tx_failed(lambda: c.foo(-1))
+    assert_tx_failed(lambda: c.foo(lo))  # note: lo < 0
+
 
 @pytest.mark.parametrize("typ,lo,hi,bits", PARAMS)
-def test_exponent_base_one(get_contract, typ, lo, hi, bits):
+def test_exponent_base_one(get_contract, assert_tx_failed, typ, lo, hi, bits):
     code = f"""
 @external
 def foo(x: {typ}) -> {typ}:
@@ -41,22 +41,24 @@ def foo(x: {typ}) -> {typ}:
     c = get_contract(code)
     assert c.foo(0) == 1
     assert c.foo(1) == 1
-    assert c.foo(-1) == 1
-    assert c.foo(lo) == 1
     assert c.foo(hi) == 1
 
+    assert_tx_failed(lambda: c.foo(-1))
+    assert_tx_failed(lambda: c.foo(lo))
+
 
 def test_exponent_base_minus_one(get_contract):
     # #2986
+    # NOTE!!! python order of precedence means -1 ** x != (-1) ** x
     code = """
 @external
-def foo() -> int256:
-    x: int256 = 4
-    y: int256 = -1 ** x
+def foo(x: int256) -> int256:
+    y: int256 = (-1) ** x
     return y
     """
     c = get_contract(code)
-    assert c.foo() == -1
+    for x in range(5):
+        assert c.foo(x) == (-1) ** x
 
 
 # TODO: make this test pass
@@ -69,9 +71,9 @@ def bar() -> int16:
     x: int16 = -2
     return {base} ** x
     """
-    get_contract(code)
+    c = get_contract(code)
     # known bug: 2985
-    # assert_tx_failed(lambda: c.bar())
+    assert_tx_failed(lambda: c.bar())
 
 
 def test_exponent_min_int16(get_contract):
@@ -87,17 +89,17 @@ def foo() -> int16:
     assert c.foo() == -(2 ** 15)
 
 
-@pytest.mark.parametrize("power", [0, 1])
-def test_exponent_power_zero_one(get_contract, power):
+@pytest.mark.parametrize("base,power", itertools.product((-2, -1, 0, 1, 2), (0, 1)))
+def test_exponent_power_zero_one(get_contract, base, power):
     # #2989
     code = f"""
 @external
 def foo() -> int256:
-    x: int256 = 2
+    x: int256 = {base}
     return x ** {power}
     """
     c = get_contract(code)
-    assert c.foo() == 2 ** power
+    assert c.foo() == base ** power
 
 
 @pytest.mark.parametrize("typ,lo,hi,bits", PARAMS)