Repository: vyperlang/vyper

Issue:
raw_call returns empty calldata
### Version Information

* vyper Version (output of `vyper --version`): latest master
* OS: linux
* Python Version (output of `python --version`): 3.8

### What's your issue about?
The `raw_call` function always returns exactly `outsize` bytes, even when the actual data returned is less than this.

```python
@public
def foo() -> bool:
    return True
```

```python
@public
def call_foo(_addr: address) -> int128:
    response: bytes[64] = raw_call(_addr, method_id("foo()", bytes[4]), outsize=64)
    return len(response)
```

In this example, `call_foo` will return `64` even though the actual response from `foo` has a length of `32`.

### How can it be fixed?
In the LLL for `raw_call`, instead of copying `outsize` bytes into memory, copy the minimum of `outsize` and `RETURNDATALENGTH`.


Discussion:
Little more context: if there is a call type that may or may not return a specific data type ('RETURNDATALENGTH' is either 0 or another number), because of this bug it would not be possible to differentiate between the two. With the fix in place, it would be possible to differentiate those two cases.

Breaking change

Resolving patch:
diff --git a/vyper/functions/functions.py b/vyper/functions/functions.py
--- a/vyper/functions/functions.py
+++ b/vyper/functions/functions.py
@@ -782,7 +782,7 @@ class RawCall:
     _id = "raw_call"
     _inputs = [("to", "address"), ("data", "bytes")]
     _kwargs = {
-        "outsize": Optional('num_literal', 0),
+        "max_outsize": Optional('num_literal', 0),
         "gas": Optional('uint256', 'gas'),
         "value": Optional('uint256', zero_value),
         "is_delegate_call": Optional('bool', false_value),
@@ -796,7 +796,7 @@ def build_LLL(self, expr, args, kwargs, context):
         gas, value, outsize, delegate_call, static_call = (
             kwargs['gas'],
             kwargs['value'],
-            kwargs['outsize'],
+            kwargs['max_outsize'],
             kwargs['is_delegate_call'],
             kwargs['is_static_call'],
         )
@@ -843,9 +843,15 @@ def build_LLL(self, expr, args, kwargs, context):
 
         # build sequence LLL
         if outsize:
-            # only copy the return value to memory if outsize > 0
+            # return minimum of outsize and returndatasize
+            size = [
+                'with', '_l', outsize, [
+                    'with', '_r', 'returndatasize', ['if', ['gt', '_l', '_r'], '_r', '_l']
+                ]
+            ]
+
             seq = [
-                'seq', copier, ['assert', call_lll], ['mstore', output_node, outsize], output_node
+                'seq', copier, ['assert', call_lll], ['mstore', output_node, size], output_node
             ]
             typ = ByteArrayType(outsize)
         else:


Test patch:
diff --git a/tests/examples/wallet/test_wallet.py b/tests/examples/wallet/test_wallet.py
--- a/tests/examples/wallet/test_wallet.py
+++ b/tests/examples/wallet/test_wallet.py
@@ -58,7 +58,9 @@ def pack_and_sign(seq, *args):
     sigs = pack_and_sign(1, k1, 0, k3, 0, k5)
     assert_tx_failed(lambda: c.approve(1, to_address, value, data, sigs, transact={'value': 0, 'from': a1}))  # noqa: E501
     sigs = pack_and_sign(1, k1, 0, k3, 0, k5)
-    assert c.approve(1, to_address, value, data, sigs, call={'value': value, 'from': a1})
+
+    # this call should succeed
+    c.approve(1, to_address, value, data, sigs, call={'value': value, 'from': a1})
 
     print("Basic tests passed")
 
@@ -104,7 +106,7 @@ def test_javascript_signatures(w3, get_contract):
 
     # There's no need to pass in signatures because the owners are 0 addresses
     # causing them to default to valid signatures
-    assert x2.approve(
+    x2.approve(
         0,
         recipient,
         25,
diff --git a/tests/parser/exceptions/test_constancy_exception.py b/tests/parser/exceptions/test_constancy_exception.py
--- a/tests/parser/exceptions/test_constancy_exception.py
+++ b/tests/parser/exceptions/test_constancy_exception.py
@@ -39,7 +39,9 @@ def foo() -> int128:
 @public
 @constant
 def foo() -> int128:
-    x = raw_call(0x1234567890123456789012345678901234567890, b"cow", outsize=4, gas=595757, value=9)
+    x = raw_call(
+        0x1234567890123456789012345678901234567890, b"cow", max_outsize=4, gas=595757, value=9
+    )
     return 5
     """,
     """
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
@@ -64,7 +64,7 @@ def foo():
     """
 @public
 def foo():
-    x: bytes[4] = raw_call(0x1234567890123456789012345678901234567890, outsize=4)
+    x: bytes[4] = raw_call(0x1234567890123456789012345678901234567890, max_outsize=4)
     """,
     """
 @public
@@ -75,13 +75,13 @@ def foo():
 @public
 def foo():
     x: bytes[4] = raw_call(
-        0x1234567890123456789012345678901234567890, b"cow", gas=111111, outsize=4, moose=9
+        0x1234567890123456789012345678901234567890, b"cow", gas=111111, max_outsize=4, moose=9
     )
     """,
     """
 @public
 def foo():
-    x: bytes[4] = create_forwarder_to(0x1234567890123456789012345678901234567890, outsize=4)
+    x: bytes[4] = create_forwarder_to(0x1234567890123456789012345678901234567890, max_outsize=4)
     """,
     """
 x: public()
diff --git a/tests/parser/exceptions/test_syntax_exception.py b/tests/parser/exceptions/test_syntax_exception.py
--- a/tests/parser/exceptions/test_syntax_exception.py
+++ b/tests/parser/exceptions/test_syntax_exception.py
@@ -59,7 +59,7 @@ def foo():
     """
 @public
 def foo():
-    x: bytes[4] = raw_call(0x123456789012345678901234567890123456789, "cow", outsize=4)
+    x: bytes[4] = raw_call(0x123456789012345678901234567890123456789, "cow", max_outsize=4)
     """,
     """
 @public
diff --git a/tests/parser/features/test_assert.py b/tests/parser/features/test_assert.py
--- a/tests/parser/features/test_assert.py
+++ b/tests/parser/features/test_assert.py
@@ -118,7 +118,7 @@ def test():
     code = """
 @public
 def test():
-    assert raw_call(msg.sender, b'', outsize=1, gas=10, value=1000*1000) == 1
+    assert raw_call(msg.sender, b'', max_outsize=1, gas=10, value=1000*1000) == 1
     """
     assert_compile_failed(lambda: get_contract(code), ConstancyViolation)
 
diff --git a/tests/parser/features/test_conditionals.py b/tests/parser/features/test_conditionals.py
--- a/tests/parser/features/test_conditionals.py
+++ b/tests/parser/features/test_conditionals.py
@@ -22,7 +22,7 @@ def test_single_branch_underflow_public(get_contract_with_gas_estimation):
 @public
 def doit():
     if False:
-        raw_call(msg.sender, b"", outsize=0, value=0, gas=msg.gas)
+        raw_call(msg.sender, b"", max_outsize=0, value=0, gas=msg.gas)
     """
     c = get_contract_with_gas_estimation(code)
     c.doit()
diff --git a/tests/parser/functions/test_method_id.py b/tests/parser/functions/test_method_id.py
--- a/tests/parser/functions/test_method_id.py
+++ b/tests/parser/functions/test_method_id.py
@@ -8,7 +8,7 @@ def double(x: int128) -> int128:
 
 @public
 def returnten() -> int128:
-    ans: bytes[32] = raw_call(self, concat(method_id("double(int128)", bytes[4]), convert(5, bytes32)), gas=50000, outsize=32)  # noqa: E501
+    ans: bytes[32] = raw_call(self, concat(method_id("double(int128)", bytes[4]), convert(5, bytes32)), gas=50000, max_outsize=32)  # noqa: E501
     return convert(convert(ans, bytes32), int128)
     """
     c = get_contract_with_gas_estimation(method_id_test)
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
@@ -5,27 +5,34 @@
 from vyper.functions import get_create_forwarder_to_bytecode
 
 
-def test_caller_code(get_contract_with_gas_estimation):
-    caller_code = """
+def test_max_outsize_exceeds_returndatasize(get_contract):
+    source_code = """
 @public
 def foo() -> bytes[7]:
-    return raw_call(0x0000000000000000000000000000000000000004, b"moose", gas=50000, outsize=5)
+    return raw_call(0x0000000000000000000000000000000000000004, b"moose", max_outsize=7)
+    """
+    c = get_contract(source_code)
+    assert c.foo() == b"moose"
 
-@public
-def bar() -> bytes[7]:
-    return raw_call(0x0000000000000000000000000000000000000004, b"moose", gas=50000, outsize=3)
 
+def test_returndatasize_exceeds_max_outsize(get_contract):
+    source_code = """
 @public
-def baz() -> bytes[7]:
-    return raw_call(0x0000000000000000000000000000000000000004, b"moose", gas=50000, outsize=7)
+def foo() -> bytes[3]:
+    return raw_call(0x0000000000000000000000000000000000000004, b"moose", max_outsize=3)
     """
+    c = get_contract(source_code)
+    assert c.foo() == b"moo"
 
-    c = get_contract_with_gas_estimation(caller_code)
-    assert c.foo() == b"moose"
-    assert c.bar() == b"moo"
-    assert c.baz() == b"moose\x00\x00"
 
-    print('Passed raw call test')
+def test_returndatasize_matches_max_outsize(get_contract):
+    source_code = """
+@public
+def foo() -> bytes[5]:
+    return raw_call(0x0000000000000000000000000000000000000004, b"moose", max_outsize=5)
+    """
+    c = get_contract(source_code)
+    assert c.foo() == b"moose"
 
 
 def test_multiple_levels(w3, get_contract_with_gas_estimation):
@@ -41,7 +48,7 @@ def returnten() -> int128:
 @public
 def create_and_call_returnten(inp: address) -> int128:
     x: address = create_forwarder_to(inp)
-    o: int128 = extract32(raw_call(x, convert("\xd0\x1f\xb1\xb8", bytes[4]), outsize=32, gas=50000), 0, type=int128)  # noqa: E501
+    o: int128 = extract32(raw_call(x, convert("\xd0\x1f\xb1\xb8", bytes[4]), max_outsize=32, gas=50000), 0, type=int128)  # noqa: E501
     return o
 
 @public
@@ -83,7 +90,7 @@ def returnten() -> int128:
 @public
 def create_and_call_returnten(inp: address) -> int128:
     x: address = create_forwarder_to(inp)
-    o: int128 = extract32(raw_call(x, convert("\xd0\x1f\xb1\xb8", bytes[4]), outsize=32, gas=50000), 0, type=int128)  # noqa: E501
+    o: int128 = extract32(raw_call(x, convert("\xd0\x1f\xb1\xb8", bytes[4]), max_outsize=32, gas=50000), 0, type=int128)  # noqa: E501
     return o
 
 @public
@@ -128,7 +135,7 @@ def set(i: int128, owner: address):
         self.owner_setter_contract,
         cdata,
         gas=msg.gas,
-        outsize=0,
+        max_outsize=0,
         is_delegate_call=True
     )
     """
@@ -168,7 +175,7 @@ def foo_call(_addr: address):
         method_id("foo(bytes32)", bytes[4]),
         0x0000000000000000000000000000000000000000000000000000000000000001
     )
-    raw_call(_addr, cdata, outsize=0{})
+    raw_call(_addr, cdata, max_outsize=0{})
     """
 
     # with no gas value given, enough will be forwarded to complete the call
@@ -200,7 +207,7 @@ def foo(_addr: address) -> int128:
     _response: bytes[32] = raw_call(
         _addr,
         method_id("foo()", bytes[4]),
-        outsize=32,
+        max_outsize=32,
         is_static_call=True,
     )
     return convert(_response, int128)
@@ -230,7 +237,7 @@ def foo(_addr: address) -> int128:
     _response: bytes[32] = raw_call(
         _addr,
         method_id("foo()", bytes[4]),
-        outsize=32,
+        max_outsize=32,
         is_static_call=True,
     )
     return convert(_response, int128)
diff --git a/tests/parser/functions/test_send.py b/tests/parser/functions/test_send.py
--- a/tests/parser/functions/test_send.py
+++ b/tests/parser/functions/test_send.py
@@ -41,7 +41,7 @@ def test_send(receiver: address):
 
 @public
 def test_call(receiver: address):
-    raw_call(receiver, b"", gas=50000, outsize=0, value=1)
+    raw_call(receiver, b"", gas=50000, max_outsize=0, value=1)
     """
 
     # default function writes variable, this requires more gas than send can pass
diff --git a/tests/parser/syntax/test_constants.py b/tests/parser/syntax/test_constants.py
--- a/tests/parser/syntax/test_constants.py
+++ b/tests/parser/syntax/test_constants.py
@@ -109,11 +109,11 @@ def test() -> int128:
 
 @private
 def test():
-   raw_call(0x0000000000000000000000000000000000000005, b'hello', outsize=TEST_C, gas=2000)
+   raw_call(0x0000000000000000000000000000000000000005, b'hello', max_outsize=TEST_C, gas=2000)
 
 @private
 def test1():
-    raw_call(0x0000000000000000000000000000000000000005, b'hello', outsize=256, gas=TEST_WEI)
+    raw_call(0x0000000000000000000000000000000000000005, b'hello', max_outsize=256, gas=TEST_WEI)
     """,
     """
 LIMIT: constant(int128) = 1
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
@@ -8,7 +8,9 @@
     ("""
 @public
 def foo():
-    x: bytes[9] = raw_call(0x1234567890123456789012345678901234567890, b"cow", outsize=4, outsize=9)
+    x: bytes[9] = raw_call(
+        0x1234567890123456789012345678901234567890, b"cow", max_outsize=4, max_outsize=9
+    )
     """, SyntaxException),
     """
 @public
@@ -23,7 +25,7 @@ def foo():
     """
 @public
 def foo():
-    # fails because raw_call without outsize does not return a value
+    # fails because raw_call without max_outsize does not return a value
     x: bytes[9] = raw_call(0x1234567890123456789012345678901234567890, b"cow")
     """,
 ]
@@ -47,7 +49,7 @@ def foo():
     x: bytes[9] = raw_call(
         0x1234567890123456789012345678901234567890,
         b"cow",
-        outsize=4,
+        max_outsize=4,
         gas=595757
     )
     """,
@@ -57,7 +59,7 @@ def foo():
     x: bytes[9] = raw_call(
         0x1234567890123456789012345678901234567890,
         b"cow",
-        outsize=4,
+        max_outsize=4,
         gas=595757,
         value=as_wei_value(9, "wei")
     )
@@ -68,7 +70,7 @@ def foo():
     x: bytes[9] = raw_call(
         0x1234567890123456789012345678901234567890,
         b"cow",
-        outsize=4,
+        max_outsize=4,
         gas=595757,
         value=9
     )