Repository: vyperlang/vyper

Issue:
Bug: can't assert result of external calls to modifiable functions 
### Version Information

* vyper Version (output of `vyper --version`): 0.1.0b10

### What's your issue about?
Upgrading Uniswap from vyper 0.1.0b4 to 0.1.0b10 and noticed my tests were failing on asserting external contract calls that return bools.

This fails (reverts):
```python
assert self.token.transferFrom(msg.sender, self, token_amount)
```

This succeeds:
```python
success: bool = self.token.transferFrom(msg.sender, self, token_amount)
assert success
```

The interface was defined as:
```
contract Token():
    def transferFrom(_from : address, _to : address, _value : uint256) -> bool: modifying

```


Discussion:
Doesn't seem to be call data causing the issue:
```python
>>> import vyper
>>> c_interface = vyper.compile_code("""
contract Foo:
    def baz() -> bool: modifying

foo: Foo

@public
def __init__(_foo: address):
    self.foo = Foo(_foo)

@public
def call_foo():
    assert self.foo.baz()
""", output_formats=['abi', 'bytecode'])

>>> foo_interface = vyper.compile_code("""
@public
def baz() -> bool:
    return True
""", output_formats=['abi', 'bytecode'])

>>> from web3 import Web3, EthereumTesterProvider
>>> w3 = Web3(EthereumTesterProvider())
>>> txn_hash = w3.eth.contract(**foo_interface).constructor().transact()
>>> address = w3.eth.waitForTransactionReceipt(txn_hash)['contractAddress']
>>> foo = w3.eth.contract(address, **foo_interface)

>>> txn_hash = w3.eth.contract(**c_interface).constructor(foo.address).transact()
>>> address = w3.eth.waitForTransactionReceipt(txn_hash)['contractAddress']
>>> contract = w3.eth.contract(address, **c_interface)

>>> contract.functions.call_foo().transact()
HexBytes('0x13583b7cb86379c320c2358df34a144e2e9f028aa1c8980af1ae14461f38deac')
```
Same bug as https://github.com/ethereum/vyper/issues/1377
I inspected the IR of both ways, and `assert <external call>` uses staticcall, then I remembered https://github.com/ethereum/vyper/issues/1150. So it is correct in using static call but incorrect in not weeding out the program.

Resolving patch:
diff --git a/vyper/functions/functions.py b/vyper/functions/functions.py
--- a/vyper/functions/functions.py
+++ b/vyper/functions/functions.py
@@ -75,6 +75,14 @@ def get_keyword(expr, keyword):
     # Leaving exception for other use cases.
     raise Exception("Keyword %s not found" % keyword)  # pragma: no cover
 
+# like `assert foo`, but doesn't check constancy.
+# currently no option for reason string (easy to add, just need to refactor
+# vyper.parser.stmt so we can use _assert_reason).
+@signature('bool')
+def assert_modifiable(expr, args, kwargs, context):
+    # cf. vyper.parser.stmt.parse_assert
+    return LLLnode.from_list(['assert', args[0]], typ=None, pos=getpos(expr))
+
 
 @signature('decimal')
 def floor(expr, args, kwargs, context):
@@ -1288,6 +1296,7 @@ def _clear():
 }
 
 stmt_dispatch_table = {
+    'assert_modifiable': assert_modifiable,
     'clear': _clear,
     'send': send,
     'selfdestruct': selfdestruct,
diff --git a/vyper/parser/external_call.py b/vyper/parser/external_call.py
--- a/vyper/parser/external_call.py
+++ b/vyper/parser/external_call.py
@@ -1,5 +1,6 @@
 from vyper import ast
 from vyper.exceptions import (
+    ConstancyViolationException,
     FunctionDeclarationException,
     StructureException,
     TypeMismatchException,
@@ -69,6 +70,16 @@ def external_contract_call(node,
         ['assert', ['extcodesize', contract_address]],
         ['assert', ['ne', 'address', contract_address]],
     ]
+    if context.is_constant() and not sig.const:
+        raise ConstancyViolationException(
+            "May not call non-constant function '%s' within %s." % (
+                method_name,
+                context.pp_constancy(),
+            ) +
+            " For asserting the result of modifiable contract calls, try assert_modifiable.",
+            node
+        )
+
     if context.is_constant() or sig.const:
         sub.append([
             'assert',
diff --git a/vyper/parser/self_call.py b/vyper/parser/self_call.py
--- a/vyper/parser/self_call.py
+++ b/vyper/parser/self_call.py
@@ -301,6 +301,8 @@ def call_self_public(stmt_expr, context, sig):
     add_gas = sig.gas  # gas of call
     inargs, inargsize, _ = pack_arguments(sig, expr_args, context, pos=getpos(stmt_expr))
     output_placeholder, returner, output_size = call_make_placeholder(stmt_expr, context, sig)
+    # don't have to use static call, trust self-call since constancy analysis
+    # already propagates.
     assert_call = [
         'assert',
         ['call', ['gas'], ['address'], 0, inargs, inargsize, output_placeholder, output_size],


Test patch:
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
@@ -1,4 +1,5 @@
 from vyper.exceptions import (
+    ConstancyViolationException,
     InvalidTypeException,
     StructureException,
     TypeMismatchException,
@@ -114,21 +115,10 @@ def set_lucky(arg1: address, arg2: int128):
     print('Successfully executed an external contract call state change')
 
 
-def test_constant_external_contract_call_cannot_change_state(assert_tx_failed,
-                                                             get_contract_with_gas_estimation):
-    contract_1 = """
-lucky: public(int128)
-
-@public
-def set_lucky(_lucky: int128) -> int128:
-    self.lucky = _lucky
-    return _lucky
-    """
-
-    lucky_number = 7
-    c = get_contract_with_gas_estimation(contract_1)
-
-    contract_2 = """
+def test_constant_external_contract_call_cannot_change_state(
+        assert_compile_failed,
+        get_contract_with_gas_estimation):
+    c = """
 contract Foo:
     def set_lucky(_lucky: int128) -> int128: modifying
 
@@ -142,11 +132,11 @@ def set_lucky_expr(arg1: address, arg2: int128):
 def set_lucky_stmt(arg1: address, arg2: int128) -> int128:
     return Foo(arg1).set_lucky(arg2)
     """
-    c2 = get_contract_with_gas_estimation(contract_2)
+    assert_compile_failed(
+            lambda: get_contract_with_gas_estimation(c),
+            ConstancyViolationException)
 
-    assert_tx_failed(lambda: c2.set_lucky_expr(c.address, lucky_number, transact={}))
-    assert_tx_failed(lambda: c2.set_lucky_stmt(c.address, lucky_number, transact={}))
-    print('Successfully tested an constant external contract call attempted state change')
+    print('Successfully blocked an external contract call from a constant function')
 
 
 def test_external_contract_can_be_changed_based_on_address(get_contract):