Repository: vyperlang/vyper

Issue:
Allow STATICCALL in raw_call
### Overview
We should add a kwarg `is_static_call` to `raw_call` that uses `STATICCALL` to complete the call.  When set to `True` it should be possible to perform a `raw_call` inside a constant function.



Discussion:
Ship it

Resolving patch:
diff --git a/vyper/functions/functions.py b/vyper/functions/functions.py
--- a/vyper/functions/functions.py
+++ b/vyper/functions/functions.py
@@ -786,25 +786,33 @@ class RawCall:
         "gas": Optional('uint256', 'gas'),
         "value": Optional('uint256', zero_value),
         "is_delegate_call": Optional('bool', false_value),
+        "is_static_call": Optional('bool', false_value),
     }
     _return_type = None
 
     @validate_inputs
     def build_LLL(self, expr, args, kwargs, context):
         to, data = args
-        gas, value, outsize, delegate_call = (
+        gas, value, outsize, delegate_call, static_call = (
             kwargs['gas'],
             kwargs['value'],
             kwargs['outsize'],
             kwargs['is_delegate_call'],
+            kwargs['is_static_call'],
         )
-        if delegate_call.typ.is_literal is False:
-            raise TypeMismatch(
-                'The delegate_call parameter has to be a static/literal boolean value.'
+        for key in ("is_delegate_call", "is_static_call"):
+            if kwargs[key].typ.is_literal is False:
+                raise TypeMismatch(
+                    f"The `{key}` parameter must be a static/literal boolean value", expr
+                )
+        if delegate_call.value and static_call.value:
+            raise ArgumentException(
+                "Call may use one of `is_delegate_call` or `is_static_call`, not both", expr
             )
-        if context.is_constant():
+        if not static_call.value and context.is_constant():
             raise ConstancyViolation(
-                f"Cannot make calls from {context.pp_constancy()}",
+                f"Cannot make modifying calls from {context.pp_constancy()},"
+                " use `is_static_call=True` to perform this action",
                 expr,
             )
         placeholder = context.new_placeholder(data.typ)
@@ -828,6 +836,8 @@ def build_LLL(self, expr, args, kwargs, context):
 
         if delegate_call.value == 1:
             call_lll = ['delegatecall', gas, to] + common_call_lll
+        elif static_call.value == 1:
+            call_lll = ['staticcall', gas, to] + common_call_lll
         else:
             call_lll = ['call', gas, to, value] + common_call_lll
 


Test patch:
diff --git a/tests/parser/functions/test_raw_call.py b/tests/parser/functions/test_raw_call.py
--- a/tests/parser/functions/test_raw_call.py
+++ b/tests/parser/functions/test_raw_call.py
@@ -1,3 +1,7 @@
+import pytest
+
+from vyper import compiler
+from vyper.exceptions import ArgumentException, ConstancyViolation
 from vyper.functions import get_create_forwarder_to_bytecode
 
 
@@ -178,3 +182,82 @@ def foo_call(_addr: address):
     # manually specifying an insufficient amount should fail
     outer_contract = get_contract(outer_code.format(", gas=15000"))
     assert_tx_failed(lambda: outer_contract.foo_call(inner_contract.address))
+
+
+def test_static_call(get_contract):
+
+    target_source = """
+@public
+@constant
+def foo() -> int128:
+    return 42
+"""
+
+    caller_source = """
+@public
+@constant
+def foo(_addr: address) -> int128:
+    _response: bytes[32] = raw_call(
+        _addr,
+        method_id("foo()", bytes[4]),
+        outsize=32,
+        is_static_call=True,
+    )
+    return convert(_response, int128)
+    """
+
+    target = get_contract(target_source)
+    caller = get_contract(caller_source)
+
+    assert caller.foo(target.address) == 42
+
+
+def test_static_call_fails_modifying(get_contract, assert_tx_failed):
+
+    target_source = """
+baz: int128
+
+@public
+def foo() -> int128:
+    self.baz = 31337
+    return self.baz
+"""
+
+    caller_source = """
+@public
+@constant
+def foo(_addr: address) -> int128:
+    _response: bytes[32] = raw_call(
+        _addr,
+        method_id("foo()", bytes[4]),
+        outsize=32,
+        is_static_call=True,
+    )
+    return convert(_response, int128)
+    """
+
+    target = get_contract(target_source)
+    caller = get_contract(caller_source)
+
+    assert_tx_failed(lambda: caller.foo(target.address))
+
+
+uncompilable_code = [
+    ("""
+@public
+@constant
+def foo(_addr: address):
+    raw_call(_addr, method_id("foo()", bytes[4]))
+    """, ConstancyViolation),
+    ("""
+@public
+def foo(_addr: address):
+    raw_call(_addr, method_id("foo()", bytes[4]), is_delegate_call=True, is_static_call=True)
+    """, ArgumentException),
+]
+
+
+@pytest.mark.parametrize('source_code,exc', uncompilable_code)
+def test_invalid_type_exception(source_code, exc):
+    with pytest.raises(exc):
+        compiler.compile_code(source_code)