Repository: vyperlang/vyper

Issue:
VIP: make gas and outsize optional arguments in raw_call
## Simple Summary
The `raw_call` builtin function should handle both `gas` and `outsize` as optional arguments, using the following defaults:

1. `gas`: `gasLeft`
2. `outsize`: `0`

This is an extension of #1879

## Motivation
### `gas`
In the majority of cases, the desired effect in a `raw_call` is to forward all remaining gas. To me this feels intuitive, even expected. I don't think it should require an explicit declaration.

### `outsize`
`outsize` is necessary for type checking when the result of a `raw_call` is assigned to a variable, but in cases where the result isn't stored it feels unnecessary.  I think that we should use a default value of `0`. In this case we can internally return `None`, which raises if assigned to any variable.

## Specification
Change the syntax in the following manner:

#### Current
```python
# sending all gas, no assignment of return value
raw_call(receiver, b"foo", 0, msg.gas)

# declaring gas forwarded and outsize
bar: bytes[32] = raw_call(receiver, b"foo", 32, 50000)
```

#### Proposed
```python
# sending all gas, no assignment of return value
raw_call(receiver, b"foo")

# declaring gas forwarded and outsize
bar: bytes[32] = raw_call(receiver, b"foo", outsize=32, gas=50000)
```

The syntax for the default behavior looks much cleaner, and the advanced use is more explicit and so easier to understand.

## Backwards Compatibility
This VIP reverses the breaking change that would be caused by #1879.

## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)



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
@@ -641,8 +641,8 @@ def as_wei_value(expr, args, kwargs, context):
 @signature(
     'address',
     'bytes',
-    outsize='num_literal',
-    gas='uint256',
+    outsize=Optional('num_literal', 0),
+    gas=Optional('uint256', 'gas'),
     value=Optional('uint256', zero_value),
     delegate_call=Optional('bool', false_value),
 )
@@ -679,54 +679,30 @@ def raw_call(expr, args, kwargs, context):
         location='memory',
     )
 
+    # build LLL for call or delegatecall
+    common_call_lll = [
+        ['add', placeholder_node, 32],
+        ['mload', placeholder_node],
+        # if there is no return value, the return offset can be 0
+        ['add', output_node, 32] if outsize else 0,
+        outsize
+    ]
+
     if delegate_call.value == 1:
-        z = LLLnode.from_list(
-            [
-                'seq',
-                copier,
-                [
-                    'assert',
-                    [
-                        'delegatecall',
-                        gas,
-                        to,
-                        ['add', placeholder_node, 32],
-                        ['mload', placeholder_node],
-                        ['add', output_node, 32],
-                        outsize,
-                    ],
-                ],
-                ['mstore', output_node, outsize],
-                output_node,
-            ],
-            typ=ByteArrayType(outsize),
-            location='memory',
-            pos=getpos(expr),
-        )
+        call_lll = ['delegatecall', gas, to] + common_call_lll
     else:
-        z = LLLnode.from_list(
-            [
-                'seq',
-                copier,
-                [
-                    'assert',
-                    [
-                        'call',
-                        gas,
-                        to,
-                        value,
-                        ['add', placeholder_node, 32],
-                        ['mload', placeholder_node],
-                        ['add', output_node, 32],
-                        outsize,
-                    ],
-                ],
-                ['mstore', output_node, outsize],
-                output_node,
-            ],
-            typ=ByteArrayType(outsize), location='memory', pos=getpos(expr)
-        )
-    return z
+        call_lll = ['call', gas, to, value] + common_call_lll
+
+    # build sequence LLL
+    if outsize:
+        # only copy the return value to memory if outsize > 0
+        seq = ['seq', copier, ['assert', call_lll], ['mstore', output_node, outsize], output_node]
+        typ = ByteArrayType(outsize)
+    else:
+        seq = ['seq', copier, ['assert', call_lll]]
+        typ = None
+
+    return LLLnode.from_list(seq, typ=typ, location="memory", pos=getpos(expr))
 
 
 @signature('address', 'uint256')


Test patch:
diff --git a/tests/parser/exceptions/test_structure_exception.py b/tests/parser/exceptions/test_structure_exception.py
--- a/tests/parser/exceptions/test_structure_exception.py
+++ b/tests/parser/exceptions/test_structure_exception.py
@@ -76,27 +76,24 @@ def foo():
     """
 @public
 def foo():
-    x = raw_call(0x1234567890123456789012345678901234567890, b"cow")
+    x: bytes[4] = raw_call(0x1234567890123456789012345678901234567890, outsize=4)
     """,
     """
 @public
 def foo():
-    x = raw_call(0x1234567890123456789012345678901234567890, outsize=4)
+    x: bytes[4] = create_forwarder_to(0x1234567890123456789012345678901234567890, b"cow")
     """,
     """
 @public
 def foo():
-    x = create_forwarder_to(0x1234567890123456789012345678901234567890, b"cow")
+    x: bytes[4] = raw_call(
+        0x1234567890123456789012345678901234567890, b"cow", gas=111111, outsize=4, moose=9
+    )
     """,
     """
 @public
 def foo():
-    x = raw_call(0x1234567890123456789012345678901234567890, b"cow", gas=111111, outsize=4, moose=9)
-    """,
-    """
-@public
-def foo():
-    x = create_forwarder_to(0x1234567890123456789012345678901234567890, outsize=4)
+    x: bytes[4] = create_forwarder_to(0x1234567890123456789012345678901234567890, outsize=4)
     """,
     """
 x: public()
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
@@ -146,3 +146,37 @@ def set(i: int128, owner: address):
     tx_hash = outer_contract.set(1, a1, transact={})
     assert w3.eth.getTransactionReceipt(tx_hash)['status'] == 1
     assert outer_contract.owners(1) == a1
+
+
+def test_gas(get_contract, assert_tx_failed):
+    inner_code = """
+bar: bytes32
+
+@public
+def foo(_bar: bytes32):
+    self.bar = _bar
+    """
+
+    inner_contract = get_contract(inner_code)
+
+    outer_code = """
+@public
+def foo_call(_addr: address):
+    cdata: bytes[40] = concat(
+        method_id("foo(bytes32)", bytes[4]),
+        0x0000000000000000000000000000000000000000000000000000000000000001
+    )
+    raw_call(_addr, cdata, outsize=0{})
+    """
+
+    # with no gas value given, enough will be forwarded to complete the call
+    outer_contract = get_contract(outer_code.format(""))
+    outer_contract.foo_call(inner_contract.address)
+
+    # manually specifying a sufficient amount should succeed
+    outer_contract = get_contract(outer_code.format(", gas=21000"))
+    outer_contract.foo_call(inner_contract.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))
diff --git a/tests/parser/syntax/test_raw_call.py b/tests/parser/syntax/test_raw_call.py
--- a/tests/parser/syntax/test_raw_call.py
+++ b/tests/parser/syntax/test_raw_call.py
@@ -26,7 +26,13 @@ def foo():
 @public
 def foo():
     raw_log([], 0x1234567890123456789012345678901234567890)
+    """,
     """
+@public
+def foo():
+    # fails because raw_call without outsize does not return a value
+    x: bytes[9] = raw_call(0x1234567890123456789012345678901234567890, b"cow")
+    """,
 ]
 
 
@@ -73,7 +79,12 @@ def foo():
         gas=595757,
         value=9
     )
+    """,
     """
+@public
+def foo():
+    raw_call(0x1234567890123456789012345678901234567890, b"cow")
+    """,
 ]