Skip to content

Commit

Permalink
feat: add default_return_value kwarg for calls (#2839)
Browse files Browse the repository at this point in the history
this commit adds a new kwarg, `default_return_value`, available to all
external calls. its purpose is to make it easier to interface with
contracts which are not compliant with their interfaces (e.g., bad ERC20
implementations) without having to drop down to `raw_call`.

since the logic for handling kwargs in `external_call.py` was getting a
little gnarly, this commit also refactors and that module a bit. for
consistency, it also switches to exclusively using the
typechecker type of the function.
  • Loading branch information
charles-cooper authored May 13, 2022
1 parent efe1dbe commit 3195701
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2324,3 +2324,62 @@ def foo(_addr: address, _addr2: address) -> int128:

assert c2.foo(c1.address, c1.address) == 123
assert_tx_failed(lambda: c2.foo(c1.address, "0x1234567890123456789012345678901234567890"))


def test_default_override(get_contract, assert_tx_failed):
bad_erc20_code = """
@external
def transfer(receiver: address, amount: uint256):
pass
"""

code = """
from vyper.interfaces import ERC20
@external
def safeTransfer(erc20: ERC20, receiver: address, amount: uint256):
assert erc20.transfer(receiver, amount, default_return_value=True)
@external
def transferBorked(erc20: ERC20, receiver: address, amount: uint256):
assert erc20.transfer(receiver, amount)
"""
bad_erc20 = get_contract(bad_erc20_code)
c = get_contract(code)

# demonstrate transfer failing
assert_tx_failed(lambda: c.transferBorked(bad_erc20.address, c.address, 0))
# would fail without default_return_value
c.safeTransfer(bad_erc20.address, c.address, 0)


def test_default_override2(get_contract, assert_tx_failed):
bad_code_1 = """
@external
def return_64_bytes() -> bool:
return True
"""

bad_code_2 = """
@external
def return_64_bytes():
pass
"""

code = """
struct BoolPair:
x: bool
y: bool
interface Foo:
def return_64_bytes() -> BoolPair: nonpayable
@external
def bar(foo: Foo):
t: BoolPair = foo.return_64_bytes(default_return_value=BoolPair({x: True, y:True}))
assert t.x and t.y
"""
bad_1 = get_contract(bad_code_1)
bad_2 = get_contract(bad_code_2)
c = get_contract(code)

# fails due to returndatasize being nonzero but also lt 64
assert_tx_failed(lambda: c.bar(bad_1.address))
c.bar(bad_2.address)
5 changes: 4 additions & 1 deletion vyper/codegen/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,7 @@ def parse_Call(self):
return arg_ir

elif isinstance(self.expr.func, vy_ast.Attribute) and self.expr.func.attr == "pop":
# TODO consider moving this to builtins
darray = Expr(self.expr.func.value, self.context).ir_node
assert len(self.expr.args) == 0
assert isinstance(darray.typ, DArrayType)
Expand All @@ -946,10 +947,12 @@ def parse_Call(self):
)

elif (
# TODO use expr.func.type.is_internal once
# type annotations are consistently available
isinstance(self.expr.func, vy_ast.Attribute)
and isinstance(self.expr.func.value, vy_ast.Name)
and self.expr.func.value.id == "self"
): # noqa: E501
):
return self_call.ir_for_self_call(self.expr, self.context)
else:
return external_call.ir_for_external_call(self.expr, self.context)
Expand Down
Loading

0 comments on commit 3195701

Please sign in to comment.