Repository: vyperlang/vyper

Issue:
Multiple issues with as_wei_value
### Version Information

* vyper Version: 0.1.0b13

### What's your issue about?

There are multiple problems with `as_wei_value`:

When using as_wei_value with an int128 argument, it produces an incorrect result:
```
@public
def f(i: int128) -> wei_value:
    return as_wei_value(i, "kwei")
```
gets translated as
```
[seq,
  [return,
    0,
    [lll,
      [seq,
        [if, [lt, calldatasize, 4], [goto, fallback]],
        [mstore, 28, [calldataload, 0]],
        [mstore, 32, 1461501637330902918203684832716283019655932542976],
        [mstore, 64, 170141183460469231731687303715884105727],
        [mstore, 96, -170141183460469231731687303715884105728],
        [mstore, 128, 1701411834604692317316873037158841057270000000000],
        [mstore, 160, -1701411834604692317316873037158841057280000000000],
        # Line 1
        [if,
          [eq, [mload, 0], 3967261028 <f(int128)>],
          [seq,
            [assert, [iszero, callvalue]],
            /* checking int128 input */ 
            [clamp,
              [mload, 96],
              [calldataload, 4],
              [mload, 64]],
            # Line 3
            [mstore, 0, [div, [mul, [calldataload, 4 <i>], 1000], 10000000000]],
            [seq_unchecked, pass, [return, 0, 32]],
            # Line 1
            stop]],
        [seq_unchecked, [label, fallback], /* Default function */ [revert, 0, 0]]],
      0]]]
```
which treats the value as a decimal and therefore divides by 10000000000, even though it shouldn't.

For `uint256` that does not happen, however, there is no overflow check.

### How can it be fixed?

The problem lies in `as_wei_value` in `functions/functions.py`, where there is a special case for uint256, but not for int128, therefore it takes the else branch which treats the argument as a decimal:
``` 
    elif value.typ.is_literal:
        if value.value <= 0:
            raise InvalidLiteralException("Negative wei value not allowed", expr)
        sub = ['mul', value.value, denom_divisor]
    elif value.typ.typ == 'uint256':
        sub = ['mul', value, denom_divisor]
    else:
        sub = ['div', ['mul', value, denom_divisor], DECIMAL_DIVISOR]
```
It would probably be a good idea to always use `elif` and throw an error if the `else` branch is reached.


Discussion:

Resolving patch:
diff --git a/vyper/functions/functions.py b/vyper/functions/functions.py
--- a/vyper/functions/functions.py
+++ b/vyper/functions/functions.py
@@ -52,7 +52,6 @@
     LLLnode,
     add_variable_offset,
     get_length,
-    get_number_as_fraction,
     getpos,
     make_byte_array_copier,
     make_byte_slice_copier,
@@ -945,38 +944,39 @@ def fetch_call_return(self, node):
     def build_LLL(self, expr, args, kwargs, context):
         value, denom_name = args[0], args[1].decode()
 
-        denom_divisor = next((v for k, v in self.wei_denoms.items() if denom_name in k), False)
-        if not denom_divisor:
-            raise InvalidLiteral(
-                f"Invalid denomination: {denom_name}, valid denominations are: "
-                f"{','.join(x[0] for x in self.wei_denoms)}",
-                expr.args[1],
-            )
-
-        # Compute the amount of wei and return that value
-        if isinstance(value, (int, Decimal)):
-            expr_args_0 = expr.args[0]
-            # On constant reference fetch value node of constant assignment.
-            if context.constants.ast_is_constant(expr.args[0]):
-                expr_args_0 = context.constants._constants_ast[expr.args[0].id]
-            numstring, num, den = get_number_as_fraction(expr_args_0, context)
-            if denom_divisor % den:
-                max_len = len(str(denom_divisor)) - 1
-                raise InvalidLiteral(
-                    f"Wei value of denomination '{denom_name}' has max {max_len} decimal places",
-                    expr.args[0],
-                )
-            sub = num * denom_divisor // den
-        elif value.typ.is_literal:
-            if value.value <= 0:
-                raise InvalidLiteral("Negative wei value not allowed", expr)
-            sub = ["mul", value.value, denom_divisor]
-        elif value.typ.typ == "uint256":
-            sub = ["mul", value, denom_divisor]
+        denom_divisor = next(v for k, v in self.wei_denoms.items() if denom_name in k)
+        if value.typ.typ == "uint256":
+            sub = [
+                "with",
+                "ans",
+                ["mul", value, denom_divisor],
+                [
+                    "seq",
+                    [
+                        "assert",
+                        ["or", ["eq", ["div", "ans", value], denom_divisor], ["iszero", value]],
+                    ],
+                    "ans",
+                ],
+            ]
+        elif value.typ.typ == "int128":
+            # signed types do not require bounds checks because the
+            # largest possible converted value will not overflow 2**256
+            sub = [
+                "seq",
+                ["assert", ["sgt", value, -1]],
+                ["mul", value, denom_divisor],
+            ]
+        elif value.typ.typ == "decimal":
+            sub = [
+                "seq",
+                ["assert", ["sgt", value, -1]],
+                ["div", ["mul", value, denom_divisor], DECIMAL_DIVISOR],
+            ]
         else:
-            sub = ["div", ["mul", value, denom_divisor], DECIMAL_DIVISOR]
+            raise CompilerPanic(f"Unexpected type: {value.typ.typ}")
 
-        return LLLnode.from_list(sub, typ=BaseType("uint256"), location=None, pos=getpos(expr),)
+        return LLLnode.from_list(sub, typ=BaseType("uint256"), location=None, pos=getpos(expr))
 
 
 zero_value = LLLnode.from_list(0, typ=BaseType("uint256"))


Test patch:
diff --git a/tests/parser/types/value/test_as_wei_value.py b/tests/parser/types/value/test_as_wei_value.py
--- a/tests/parser/types/value/test_as_wei_value.py
+++ b/tests/parser/types/value/test_as_wei_value.py
@@ -1,30 +1,93 @@
-def test_wei_conversion(get_contract_with_gas_estimation):
-    test_wei = """
-@external
-def return_2_finney() -> uint256:
-    return as_wei_value(2, "finney")
+from decimal import Decimal
+
+import pytest
+
+wei_denoms = {
+    "femtoether": 3,
+    "kwei": 3,
+    "babbage": 3,
+    "picoether": 6,
+    "mwei": 6,
+    "lovelace": 6,
+    "nanoether": 9,
+    "gwei": 9,
+    "shannon": 9,
+    "microether": 12,
+    "szabo": 12,
+    "milliether": 15,
+    "finney": 15,
+    "ether": 18,
+    "kether": 21,
+    "grand": 21,
+}
 
+
+@pytest.mark.parametrize("denom,multiplier", wei_denoms.items())
+def test_wei_uint256(get_contract, assert_tx_failed, denom, multiplier):
+    code = f"""
 @external
-def return_3_finney() -> uint256:
-    return as_wei_value(2 + 1, "finney")
+def foo(a: uint256) -> uint256:
+    return as_wei_value(a, "{denom}")
+    """
+
+    c = get_contract(code)
 
+    value = (2 ** 256 - 1) // (10 ** multiplier)
+    assert c.foo(value) == value * (10 ** multiplier)
+
+    value = (2 ** 256 - 1) // (10 ** (multiplier - 1))
+    assert_tx_failed(lambda: c.foo(value))
+
+
+@pytest.mark.parametrize("denom,multiplier", wei_denoms.items())
+def test_wei_int128(get_contract, assert_tx_failed, denom, multiplier):
+    code = f"""
 @external
-def return_2p5_ether() -> uint256:
-    return as_wei_value(2.5, "ether")
+def foo(a: int128) -> uint256:
+    return as_wei_value(a, "{denom}")
+    """
+
+    c = get_contract(code)
+    value = (2 ** 127 - 1) // (10 ** multiplier)
 
+    assert c.foo(value) == value * (10 ** multiplier)
+
+
+@pytest.mark.parametrize("denom,multiplier", wei_denoms.items())
+def test_wei_decimal(get_contract, assert_tx_failed, denom, multiplier):
+    code = f"""
 @external
-def return_3p5_ether() -> uint256:
-    return as_wei_value(2.5 + 1.0, "ether")
+def foo(a: decimal) -> uint256:
+    return as_wei_value(a, "{denom}")
+    """
+
+    c = get_contract(code)
+    value = Decimal((2 ** 127 - 1) / (10 ** multiplier))
+
+    assert c.foo(value) == value * (10 ** multiplier)
 
+
+@pytest.mark.parametrize("value", (-1, -(2 ** 127)))
+@pytest.mark.parametrize("data_type", ["decimal", "int128"])
+def test_negative_value_reverts(get_contract, assert_tx_failed, value, data_type):
+    code = f"""
 @external
-def return_2pow64_wei() -> uint256:
-    return as_wei_value(18446744073.709551616, "gwei")
+def foo(a: {data_type}) -> uint256:
+    return as_wei_value(a, "ether")
     """
 
-    c = get_contract_with_gas_estimation(test_wei)
+    c = get_contract(code)
+    assert_tx_failed(lambda: c.foo(value))
+
+
+@pytest.mark.parametrize("denom,multiplier", wei_denoms.items())
+@pytest.mark.parametrize("data_type", ["decimal", "int128", "uint256"])
+def test_zero_value(get_contract, assert_tx_failed, denom, multiplier, data_type):
+    code = f"""
+@external
+def foo(a: {data_type}) -> uint256:
+    return as_wei_value(a, "{denom}")
+    """
 
-    assert c.return_2_finney() == 2 * 10 ** 15
-    assert c.return_3_finney() == 3 * 10 ** 15, c.return_3_finney()
-    assert c.return_2p5_ether() == 2.5 * 10 ** 18
-    assert c.return_3p5_ether() == 3.5 * 10 ** 18
-    assert c.return_2pow64_wei() == 2 ** 64
+    c = get_contract(code)
+    assert c.foo(0) == 0