Repository: vyperlang/vyper

Issue:
`int128` exponentiation can overflow without error
```python
@public
@constant
def test(x: int128, y: int128) -> int128:
    return x**y # BUG: test(2, 256) returns 0
```

The relevant compiler code is here:

https://github.com/ethereum/vyper/blob/c296b2d7532d913103aad494b749f8179a3acddc/vyper/parser/expr.py#L659-L667


Discussion:
Genericize safemath operations here over all types
similar: https://github.com/ethereum/vyper/issues/1684

Resolving patch:
diff --git a/vyper/context/types/value/numeric.py b/vyper/context/types/value/numeric.py
--- a/vyper/context/types/value/numeric.py
+++ b/vyper/context/types/value/numeric.py
@@ -5,6 +5,7 @@
 from vyper.context.types.bases import (
     BasePrimitive,
     BaseTypeDefinition,
+    CompilerPanic,
     ValueTypeDefinition,
 )
 from vyper.exceptions import InvalidOperation, OverflowException
@@ -16,12 +17,18 @@ class _NumericDefinition(ValueTypeDefinition):
 
     Attributes
     ----------
+    _bits : int
+        Number of bits the value occupies in memory
+    _is_signed : bool
+        Is the value signed?
     _invalid_op : VyperNode, optional
         Vyper ast node, or list of nodes, that is not a valid operator for binary
         operations on this type.
     """
 
     _invalid_op: Optional[Type[vy_ast.VyperNode]] = None
+    _bits: int
+    _is_signed: bool
 
     def validate_numeric_op(
         self, node: Union[vy_ast.UnaryOp, vy_ast.BinOp, vy_ast.AugAssign]
@@ -29,11 +36,74 @@ def validate_numeric_op(
         if self._invalid_op and isinstance(node.op, self._invalid_op):
             raise InvalidOperation(f"Cannot perform {node.op.description} on {self}", node)
 
+        if isinstance(node.op, vy_ast.Pow):
+            if isinstance(node, vy_ast.BinOp):
+                left, right = node.left, node.right
+            elif isinstance(node, vy_ast.AugAssign):
+                left, right = node.target, node.value
+            else:
+                raise CompilerPanic(f"Unexpected node type for numeric op: {type(node).__name__}")
+
+            value_bits = self._bits - (1 if self._is_signed else 0)
+
+            # constant folding ensures one of `(left, right)` is never a literal
+            if isinstance(left, vy_ast.Int):
+                if left.value >= 2 ** value_bits:
+                    raise OverflowException(
+                        "Base is too large, calculation will always overflow", left
+                    )
+                elif left.value < -(2 ** value_bits):
+                    raise OverflowException(
+                        "Base is too small, calculation will always underflow", left
+                    )
+            elif isinstance(right, vy_ast.Int):
+                if right.value < 0:
+                    raise InvalidOperation("Cannot calculate a negative power", right)
+                if right.value > value_bits:
+                    raise OverflowException(
+                        "Power is too large, calculation will always overflow", right
+                    )
+            else:
+                msg = (
+                    "Cannot apply an overflow check on exponentiation when both "
+                    "the base and power are unknown at compile-time."
+                )
+                if not self._is_signed:
+                    msg = (
+                        f"{msg} To perform this operation without an overflow check, use "
+                        f"`pow_mod256({left.node_source_code}, {right.node_source_code})`"
+                    )
+                raise InvalidOperation(msg, node)
+
     def validate_comparator(self, node: vy_ast.Compare) -> None:
         # all comparators are valid on numeric types
         return
 
 
+class _SignedIntegerDefinition(_NumericDefinition):
+    """
+    Private base class for signed integer definitions.
+    """
+
+    _is_signed = True
+
+    @property
+    def _id(self):
+        return f"int{self._bits}"
+
+
+class _UnsignedIntegerDefinition(_NumericDefinition):
+    """
+    Private base class for unsigned integer definitions.
+    """
+
+    _is_signed = False
+
+    @property
+    def _id(self):
+        return f"uint{self._bits}"
+
+
 class _NumericPrimitive(BasePrimitive):
 
     _as_array = True
@@ -53,17 +123,19 @@ def from_literal(cls, node: vy_ast.Constant) -> BaseTypeDefinition:
 # definitions
 
 
-class Int128Definition(IntegerAbstractType, _NumericDefinition):
-    _id = "int128"
+class Int128Definition(IntegerAbstractType, _SignedIntegerDefinition):
+    _bits = 128
 
 
-class Uint256Definition(IntegerAbstractType, _NumericDefinition):
-    _id = "uint256"
+class Uint256Definition(IntegerAbstractType, _UnsignedIntegerDefinition):
+    _bits = 256
     _invalid_op = vy_ast.USub
 
 
 class DecimalDefinition(FixedAbstractType, _NumericDefinition):
+    _bits = 168
     _id = "decimal"
+    _is_signed = True
     _invalid_op = vy_ast.Pow
 
 
diff --git a/vyper/functions/functions.py b/vyper/functions/functions.py
--- a/vyper/functions/functions.py
+++ b/vyper/functions/functions.py
@@ -1391,6 +1391,29 @@ class MulMod(_AddMulMod):
     _opcode = "mulmod"
 
 
+class PowMod256(_SimpleBuiltinFunction):
+    _id = "pow_mod256"
+    _inputs = [("a", Uint256Definition()), ("b", Uint256Definition())]
+    _return_type = Uint256Definition()
+
+    def evaluate(self, node):
+        validate_call_args(node, 2)
+        if next((i for i in node.args if not isinstance(i, vy_ast.Int)), None):
+            raise UnfoldableNode
+
+        left, right = node.args
+        if left.value < 0 or right.value < 0:
+            raise UnfoldableNode
+
+        value = (left.value ** right.value) % (2 ** 256)
+        return vy_ast.Int.from_node(node, value=value)
+
+    def build_LLL(self, expr, context):
+        left = Expr.parse_value_expr(expr.args[0], context)
+        right = Expr.parse_value_expr(expr.args[1], context)
+        return LLLnode.from_list(["exp", left, right], typ=left.typ, pos=getpos(expr))
+
+
 def get_create_forwarder_to_bytecode():
     from vyper.compile_lll import assembly_to_evm, num_to_bytearray
 
@@ -1651,6 +1674,7 @@ def build_LLL(self, expr, args, kwargs, context):
     "bitwise_not": BitwiseNot(),
     "uint256_addmod": AddMod(),
     "uint256_mulmod": MulMod(),
+    "pow_mod256": PowMod256(),
     "sqrt": Sqrt(),
     "shift": Shift(),
     "create_forwarder_to": CreateForwarderTo(),
diff --git a/vyper/parser/expr.py b/vyper/parser/expr.py
--- a/vyper/parser/expr.py
+++ b/vyper/parser/expr.py
@@ -1,5 +1,9 @@
+import math
+from decimal import Decimal, getcontext
+
 from vyper import ast as vy_ast
 from vyper.exceptions import (
+    CompilerPanic,
     EvmVersionException,
     StructureException,
     TypeCheckFailure,
@@ -57,6 +61,116 @@
     "chain",
 }
 
+# Necessary to ensure we have enough precision to do the log/exp calcs
+getcontext().prec = 42
+
+
+def calculate_largest_power(a: int, num_bits: int, is_signed: bool) -> int:
+    """
+    For a given base `a`, compute the maximum power `b` that will not
+    produce an overflow in the equation `a ** b`
+
+    Arguments
+    ---------
+    a : int
+        Base value for the equation `a ** b`
+    num_bits : int
+        The maximum number of bits that the resulting value must fit in
+    is_signed : bool
+        Is the operation being performed on signed integers?
+
+    Returns
+    -------
+    int
+        Largest possible value for `b` where the result does not overflow
+        `num_bits`
+    """
+    if num_bits % 8:
+        raise CompilerPanic("Type is not a modulo of 8")
+
+    value_bits = num_bits - (1 if is_signed else 0)
+    if a >= 2 ** value_bits:
+        raise TypeCheckFailure("Value is too large and will always throw")
+    elif a < -(2 ** value_bits):
+        raise TypeCheckFailure("Value is too small and will always throw")
+
+    a_is_negative = a < 0
+    a = abs(a)  # No longer need to know if it's signed or not
+    if a in (0, 1):
+        raise CompilerPanic("Exponential operation is useless!")
+
+    # NOTE: There is an edge case if `a` were left signed where the following
+    #       operation would not work (`ln(a)` is undefined if `a <= 0`)
+    b = int(Decimal(value_bits) / (Decimal(a).ln() / Decimal(2).ln()))
+    if b <= 1:
+        return 1  # Value is assumed to be in range, therefore power of 1 is max
+
+    # Do a bit of iteration to ensure we have the exact number
+    num_iterations = 0
+    while a ** (b + 1) < 2 ** value_bits:
+        b += 1
+        num_iterations += 1
+        assert num_iterations < 10000
+    while a ** b >= 2 ** value_bits:
+        b -= 1
+        num_iterations += 1
+        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
+    if a_is_negative and (-a) ** (b + 1) == -(2 ** value_bits):  # NOTE: a = abs(a)
+        return b + 1
+    else:
+        return b  # Exact
+
+
+def calculate_largest_base(b: int, num_bits: int, is_signed: bool) -> int:
+    """
+    For a given power `b`, compute the maximum base `a` that will not produce an
+    overflow in the equation `a ** b`
+
+    Arguments
+    ---------
+    b : int
+        Power value for the equation `a ** b`
+    num_bits : int
+        The maximum number of bits that the resulting value must fit in
+    is_signed : bool
+        Is the operation being performed on signed integers?
+
+    Returns
+    -------
+    int
+        Largest possible value for `a` where the result does not overflow
+        `num_bits`
+    """
+    if num_bits % 8:
+        raise CompilerPanic("Type is not a modulo of 8")
+    if b < 0:
+        raise TypeCheckFailure("Cannot calculate negative exponents")
+
+    value_bits = num_bits - (1 if is_signed else 0)
+    if b > value_bits:
+        raise TypeCheckFailure("Value is too large and will always throw")
+    elif b < 2:
+        return 2 ** value_bits - 1  # Maximum value for type
+
+    # Estimate (up to ~39 digits precision required)
+    a = math.ceil(2 ** (Decimal(value_bits) / Decimal(b)))
+    # Do a bit of iteration to ensure we have the exact number
+    num_iterations = 0
+    while (a + 1) ** b < 2 ** value_bits:
+        a += 1
+        num_iterations += 1
+        assert num_iterations < 10000
+    while a ** b >= 2 ** value_bits:
+        a -= 1
+        num_iterations += 1
+        assert num_iterations < 10000
+
+    return a
+
 
 def get_min_val_for_type(typ: str) -> int:
     key = "MIN_" + typ.upper()
@@ -419,23 +533,41 @@ def parse_BinOp(self):
                 return
             new_typ = BaseType(ltyp)
 
-            if ltyp == rtyp == "uint256":
-                arith = [
-                    "seq",
-                    [
-                        "assert",
-                        [
-                            "or",
-                            # r == 1 | iszero(r)
-                            # could be simplified to ~(r & 1)
-                            ["or", ["eq", "r", 1], ["iszero", "r"]],
-                            ["lt", "l", ["exp", "l", "r"]],
-                        ],
-                    ],
-                    ["exp", "l", "r"],
-                ]
-            elif ltyp == rtyp == "int128":
-                arith = ["exp", "l", "r"]
+            if self.expr.left.get("value") == 1:
+                return LLLnode.from_list([1], typ=new_typ, pos=pos)
+            if self.expr.left.get("value") == 0:
+                return LLLnode.from_list(["iszero", right], typ=new_typ, pos=pos)
+
+            if ltyp == "int128":
+                is_signed = True
+                num_bits = 128
+            else:
+                is_signed = False
+                num_bits = 256
+
+            if isinstance(self.expr.left, vy_ast.Int):
+                value = self.expr.left.value
+                upper_bound = calculate_largest_power(value, num_bits, is_signed) + 1
+                # for signed integers, this also prevents negative values
+                clamp = ["lt", right, upper_bound]
+                return LLLnode.from_list(
+                    ["seq", ["assert", clamp], ["exp", left, right]], typ=new_typ, pos=pos,
+                )
+            elif isinstance(self.expr.right, vy_ast.Int):
+                value = self.expr.right.value
+                upper_bound = calculate_largest_base(value, num_bits, is_signed) + 1
+                if is_signed:
+                    clamp = ["and", ["slt", left, upper_bound], ["sgt", left, -upper_bound]]
+                else:
+                    clamp = ["lt", left, upper_bound]
+                return LLLnode.from_list(
+                    ["seq", ["assert", clamp], ["exp", left, right]], typ=new_typ, pos=pos,
+                )
+            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
 
         if arith is None:
             return


Test patch:
diff --git a/tests/ast/nodes/test_evaluate_binop_int.py b/tests/ast/nodes/test_evaluate_binop_int.py
--- a/tests/ast/nodes/test_evaluate_binop_int.py
+++ b/tests/ast/nodes/test_evaluate_binop_int.py
@@ -67,6 +67,7 @@ def foo(a: uint256, b: uint256) -> uint256:
         assert_tx_failed(lambda: contract.foo(left, right))
 
 
+@pytest.mark.xfail(reason="need to implement safe exponentiation logic")
 @pytest.mark.fuzzing
 @settings(max_examples=50, deadline=1000)
 @given(
diff --git a/tests/functions/folding/test_powmod.py b/tests/functions/folding/test_powmod.py
new file mode 100644
--- /dev/null
+++ b/tests/functions/folding/test_powmod.py
@@ -0,0 +1,26 @@
+import pytest
+from hypothesis import given, settings
+from hypothesis import strategies as st
+
+from vyper import ast as vy_ast
+from vyper import functions as vy_fn
+
+st_uint256 = st.integers(min_value=0, max_value=256)
+
+
+@pytest.mark.fuzzing
+@settings(max_examples=100, deadline=1000)
+@given(a=st_uint256, b=st_uint256)
+def test_powmod_uint256(get_contract, a, b):
+    source = """
+@public
+def foo(a: uint256, b: uint256) -> uint256:
+    return pow_mod256(a, b)
+    """
+    contract = get_contract(source)
+
+    vyper_ast = vy_ast.parse_to_ast(f"pow_mod256({a}, {b})")
+    old_node = vyper_ast.body[0].value
+    new_node = vy_fn.PowMod256().evaluate(old_node)
+
+    assert contract.foo(a, b) == new_node.value
diff --git a/tests/fuzzing/test_exponents.py b/tests/fuzzing/test_exponents.py
new file mode 100644
--- /dev/null
+++ b/tests/fuzzing/test_exponents.py
@@ -0,0 +1,121 @@
+import pytest
+from hypothesis import example, given, settings
+from hypothesis import strategies as st
+
+from vyper.parser.expr import calculate_largest_base, calculate_largest_power
+
+
+@pytest.mark.fuzzing
+@pytest.mark.parametrize("power", range(2, 255))
+def test_exp_uint256(get_contract, assert_tx_failed, power):
+    code = f"""
+@public
+def foo(a: uint256) -> uint256:
+    return a ** {power}
+    """
+    max_base = calculate_largest_base(power, 256, False)
+    assert max_base ** power < 2 ** 256
+    assert (max_base + 1) ** power >= 2 ** 256
+
+    c = get_contract(code)
+
+    c.foo(max_base)
+    assert_tx_failed(lambda: c.foo(max_base + 1))
+
+
+@pytest.mark.fuzzing
+@pytest.mark.parametrize("power", range(2, 127))
+def test_exp_int128(get_contract, assert_tx_failed, power):
+    code = f"""
+@public
+def foo(a: int128) -> int128:
+    return a ** {power}
+    """
+    max_base = calculate_largest_base(power, 128, True)
+
+    assert -(2 ** 127) <= max_base ** power < 2 ** 127
+    assert -(2 ** 127) <= (-max_base) ** power < 2 ** 127
+
+    assert not -(2 ** 127) <= (max_base + 1) ** power < 2 ** 127
+    assert not -(2 ** 127) <= (-(max_base + 1)) ** power < 2 ** 127
+
+    c = get_contract(code)
+
+    c.foo(max_base)
+    c.foo(-max_base)
+
+    assert_tx_failed(lambda: c.foo(max_base + 1))
+    assert_tx_failed(lambda: c.foo(-max_base - 1))
+
+
+@pytest.mark.fuzzing
+@given(a=st.integers(min_value=2, max_value=2 ** 256 - 1))
+# 8 bits
+@example(a=2 ** 7)
+@example(a=2 ** 7 - 1)
+# 16 bits
+@example(a=2 ** 15)
+@example(a=2 ** 15 - 1)
+# 32 bits
+@example(a=2 ** 31)
+@example(a=2 ** 31 - 1)
+# 64 bits
+@example(a=2 ** 63)
+@example(a=2 ** 63 - 1)
+# 128 bits
+@example(a=2 ** 127)
+@example(a=2 ** 127 - 1)
+# 256 bits
+@example(a=2 ** 256 - 1)
+@settings(max_examples=200, deadline=1000)
+def test_max_exp(get_contract, assert_tx_failed, a):
+    code = f"""
+@public
+def foo(b: uint256) -> uint256:
+    return {a} ** b
+    """
+
+    c = get_contract(code)
+
+    max_power = calculate_largest_power(a, 256, False)
+
+    assert a ** max_power < 2 ** 256
+    assert a ** (max_power + 1) >= 2 ** 256
+
+    c.foo(max_power)
+    assert_tx_failed(lambda: c.foo(max_power + 1))
+
+
+@pytest.mark.fuzzing
+@given(a=st.integers(min_value=2, max_value=2 ** 127 - 1))
+# 8 bits
+@example(a=2 ** 7)
+@example(a=2 ** 7 - 1)
+# 16 bits
+@example(a=2 ** 15)
+@example(a=2 ** 15 - 1)
+# 32 bits
+@example(a=2 ** 31)
+@example(a=2 ** 31 - 1)
+# 64 bits
+@example(a=2 ** 63)
+@example(a=2 ** 63 - 1)
+# 128 bits
+@example(a=2 ** 127 - 1)
+@settings(max_examples=200, deadline=1000)
+def test_max_exp_int128(get_contract, assert_tx_failed, a):
+    code = f"""
+@public
+def foo(b: int128) -> int128:
+    return {a} ** b
+    """
+
+    c = get_contract(code)
+
+    max_power = calculate_largest_power(a, 128, True)
+
+    assert -(2 ** 127) <= a ** max_power < 2 ** 127
+    assert not -(2 ** 127) <= a ** (max_power + 1) < 2 ** 127
+
+    c.foo(max_power)
+    assert_tx_failed(lambda: c.foo(max_power + 1))
diff --git a/tests/parser/types/numbers/test_int128.py b/tests/parser/types/numbers/test_int128.py
--- a/tests/parser/types/numbers/test_int128.py
+++ b/tests/parser/types/numbers/test_int128.py
@@ -3,20 +3,32 @@
 from vyper.exceptions import OverflowException
 
 
-def test_exponents_with_nums(get_contract_with_gas_estimation):
-    exp_code = """
+def test_exponent_base_zero(get_contract):
+    code = """
 @public
-def _num_exp(x: int128, y: int128) -> int128:
-    return x**y
+def foo(x: int128) -> int128:
+    return 0 ** x
     """
+    c = get_contract(code)
+    assert c.foo(0) == 1
+    assert c.foo(1) == 0
+    assert c.foo(-1) == 0
+    assert c.foo(2 ** 127 - 1) == 0
+    assert c.foo(-(2 ** 127)) == 0
 
-    c = get_contract_with_gas_estimation(exp_code)
-    assert c._num_exp(2, 2) == 4
-    assert c._num_exp(2, 3) == 8
-    assert c._num_exp(2, 4) == 16
-    assert c._num_exp(3, 2) == 9
-    assert c._num_exp(3, 3) == 27
-    assert c._num_exp(72, 19) == 72 ** 19
+
+def test_exponent_base_one(get_contract):
+    code = """
+@public
+def foo(x: int128) -> int128:
+    return 1 ** x
+    """
+    c = get_contract(code)
+    assert c.foo(0) == 1
+    assert c.foo(1) == 1
+    assert c.foo(-1) == 1
+    assert c.foo(2 ** 127 - 1) == 1
+    assert c.foo(-(2 ** 127)) == 1
 
 
 def test_num_divided_by_num(get_contract_with_gas_estimation):
@@ -177,20 +189,6 @@ def num_mul(a: int128, b: int128) -> int128:
     assert_tx_failed(lambda: c.num_mul(2 ** 126, 2))
 
 
-def test_overflow_pow_vars(get_contract, assert_tx_failed):
-    code = """
-@public
-def num_pow(a: int128, b: int128) -> int128:
-    return a ** b
-    """
-
-    c = get_contract(code)
-
-    assert c.num_pow(-2, 127) == (-(2 ** 127))
-    assert c.num_pow(2, 126) == (2 ** 126)
-    assert_tx_failed(lambda: c.num_pow(2 ** 126, 2))
-
-
 def test_literal_int_division(get_contract):
     code = """
 @public
diff --git a/tests/parser/types/numbers/test_uint256.py b/tests/parser/types/numbers/test_uint256.py
--- a/tests/parser/types/numbers/test_uint256.py
+++ b/tests/parser/types/numbers/test_uint256.py
@@ -1,3 +1,29 @@
+def test_exponent_base_zero(get_contract):
+    code = """
+@public
+def foo(x: uint256) -> uint256:
+    return 0 ** x
+    """
+    c = get_contract(code)
+    assert c.foo(0) == 1
+    assert c.foo(1) == 0
+    assert c.foo(42) == 0
+    assert c.foo(2 ** 256 - 1) == 0
+
+
+def test_exponent_base_one(get_contract):
+    code = """
+@public
+def foo(x: uint256) -> uint256:
+    return 1 ** x
+    """
+    c = get_contract(code)
+    assert c.foo(0) == 1
+    assert c.foo(1) == 1
+    assert c.foo(42) == 1
+    assert c.foo(2 ** 256 - 1) == 1
+
+
 def test_uint256_code(assert_tx_failed, get_contract_with_gas_estimation):
     uint256_code = """
 @public
@@ -101,23 +127,6 @@ def _uint256_mulmod(x: uint256, y: uint256, z: uint256) -> uint256:
     assert_tx_failed(lambda: c._uint256_mulmod(2, 2, 0))
 
 
-def test_uint256_with_exponents(assert_tx_failed, get_contract_with_gas_estimation):
-    exp_code = """
-@public
-def _uint256_exp(x: uint256, y: uint256) -> uint256:
-        return x ** y
-    """
-
-    c = get_contract_with_gas_estimation(exp_code)
-
-    assert c._uint256_exp(2, 0) == 1
-    assert c._uint256_exp(2, 1) == 2
-    assert c._uint256_exp(2, 3) == 8
-    assert_tx_failed(lambda: c._uint256_exp(2 ** 128, 2))
-    assert c._uint256_exp(2 ** 64, 2) == 2 ** 128
-    assert c._uint256_exp(7 ** 23, 3) == 7 ** 69
-
-
 def test_modmul(get_contract_with_gas_estimation):
     modexper = """
 @public