Repository: vyperlang/vyper

Issue:
Revert messages in `raise` or `assert` are not properly propagated when the exception is rethrown by the caller
### Version Information

* vyper Version (output of `vyper --version`): 0.3.0+commit.8a23feb
* OS: Release Linux Mint 20.2 Uma 64-bit
* Python Version (output of `python --version`): Python 3.8.10

### What's your issue about?

When a contract `A` calls a contract `B`, and the second call reverts with a message, then the original call will revert as described in the documentation, but it will not propagate the revert message it got from `B`.

Example:

TraceTest.vy:
```vyper
@external
def run():
    raise "Mayday, mayday!"
```

Caller.vy:
```vyper
import TraceTest as Test

test: address

@external
def run():
    Test(self.test).run()

@external
def __init__(test: address):
    self.test = test
```

Deploy `TraceTest`, then deploy `Caller`, passing the address of `TraceTest` as an argument.  Then run `TraceTest.run()`.

The call will revert, but there will be no revert message.  The revert message ought to be, `"Mayday, mayday!"`.


Discussion:
good catch! cc @charles-cooper 

Resolving patch:
diff --git a/vyper/builtin_functions/functions.py b/vyper/builtin_functions/functions.py
--- a/vyper/builtin_functions/functions.py
+++ b/vyper/builtin_functions/functions.py
@@ -26,6 +26,7 @@
 from vyper.old_codegen.keccak256_helper import keccak256_helper
 from vyper.old_codegen.parser_utils import (
     LLLnode,
+    check_external_call,
     clamp_basetype,
     get_bytearray_length,
     get_element_ptr,
@@ -1172,7 +1173,7 @@ def build_LLL(self, expr, args, kwargs, context):
 
             if revert_on_failure:
                 typ = bytes_ty
-                ret_lll = ["seq", ["assert", call_lll], store_output_size]
+                ret_lll = ["seq", check_external_call(call_lll), store_output_size]
             else:
                 typ = TupleType([bool_ty, bytes_ty])
                 ret_lll = [
@@ -1186,7 +1187,7 @@ def build_LLL(self, expr, args, kwargs, context):
         else:
             if revert_on_failure:
                 typ = None
-                ret_lll = ["assert", call_lll]
+                ret_lll = check_external_call(call_lll)
             else:
                 typ = bool_ty
                 ret_lll = call_lll
diff --git a/vyper/old_codegen/external_call.py b/vyper/old_codegen/external_call.py
--- a/vyper/old_codegen/external_call.py
+++ b/vyper/old_codegen/external_call.py
@@ -5,6 +5,7 @@
 from vyper.old_codegen.lll_node import Encoding, LLLnode
 from vyper.old_codegen.parser_utils import (
     calculate_type_for_external_return,
+    check_external_call,
     get_element_ptr,
     getpos,
     unwrap_location,
@@ -153,7 +154,7 @@ def _external_call_helper(
     else:
         call_op = ["call", gas, contract_address, value, args_ofst, args_len, ret_ofst, ret_len]
 
-    sub.append(["assert", call_op])
+    sub.append(check_external_call(call_op))
 
     if contract_sig.return_type is not None:
         sub += ret_unpacker
diff --git a/vyper/old_codegen/parser_utils.py b/vyper/old_codegen/parser_utils.py
--- a/vyper/old_codegen/parser_utils.py
+++ b/vyper/old_codegen/parser_utils.py
@@ -42,6 +42,15 @@ def __setattr__(self, name, value):
 setcontext(DecimalContextOverride(prec=78))
 
 
+# propagate revert message when calls to external contracts fail
+def check_external_call(call_lll):
+    copy_revertdata = ["returndatacopy", 0, 0, "returndatasize"]
+    revert = ["revert", 0, "returndatasize"]
+
+    propagate_revert_lll = ["seq", copy_revertdata, revert]
+    return ["if", ["iszero", call_lll], propagate_revert_lll]
+
+
 def type_check_wrapper(fn):
     def _wrapped(*args, **kwargs):
         return_value = fn(*args, **kwargs)


Test patch:
diff --git a/tests/base_conftest.py b/tests/base_conftest.py
--- a/tests/base_conftest.py
+++ b/tests/base_conftest.py
@@ -155,6 +155,7 @@ def assert_tx_failed(function_to_test, exception=TransactionFailed, exc_text=Non
             function_to_test()
         tester.revert_to_snapshot(snapshot_id)
         if exc_text:
-            assert exc_text in str(excinfo.value)
+            # TODO test equality
+            assert exc_text in str(excinfo.value), (exc_text, excinfo.value)
 
     return assert_tx_failed
diff --git a/tests/parser/features/external_contracts/test_external_contract_calls.py b/tests/parser/features/external_contracts/test_external_contract_calls.py
--- a/tests/parser/features/external_contracts/test_external_contract_calls.py
+++ b/tests/parser/features/external_contracts/test_external_contract_calls.py
@@ -114,6 +114,28 @@ def get_array(arg1: address) -> Bytes[3]:
     assert_tx_failed(lambda: c2.get_array(c.address))
 
 
+@pytest.mark.parametrize(
+    "revert_string", ["Mayday, mayday!", "A very long revert string" + "." * 512]
+)
+def test_revert_propagation(get_contract, assert_tx_failed, revert_string):
+    raiser = f"""
+@external
+def run():
+    raise "{revert_string}"
+    """
+    caller = """
+interface Raises:
+    def run(): pure
+
+@external
+def run(raiser: address):
+    Raises(raiser).run()
+    """
+    c1 = get_contract(raiser)
+    c2 = get_contract(caller)
+    assert_tx_failed(lambda: c2.run(c1.address), exc_text=revert_string)
+
+
 @pytest.mark.parametrize("a,b", [(3, 3), (4, 3), (3, 4), (32, 32), (33, 33), (64, 64)])
 @pytest.mark.parametrize("actual", [3, 32, 64])
 def test_tuple_with_bytes(get_contract, assert_tx_failed, a, b, actual, memory_mocker):
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
@@ -4,6 +4,7 @@
 
 
 # web3 returns f"execution reverted: {err_str}"
+# TODO move exception string parsing logic into assert_tx_failed
 def _fixup_err_str(s):
     return s.replace("execution reverted: ", "")