vyper
vyper copied to clipboard
VIP: syntactic distinction for external vs internal calls
Simple Summary
Add a syntactic distinction between internal and external calls. This could be in the form of a keyword, e.g. await SomeInterface(msg.sender).foo(). The keyword could also be call.
Motivation
Internal and external calls have very different semantics. External calls invoke the CALL opcode and pass execution context to another (probably untrusted) contract, while internal calls are "safe" in that they just JUMP around the local contract. This is already reflected in the semantics of internal vs external calls, for instance external calls support the keywords gas=, value=, skip_contract_check=, default_return_value=, which are not supported for internal calls.
Right now, it is relatively easy to tell visually whether a function call is an internal or external call, as internal calls will all use the self namespace (e.g. self.foo(), as opposed to self.bar.foo()). However, as we move towards a more complex module system in vyper (cf. https://github.com/vyperlang/vyper/issues/1954, https://github.com/vyperlang/vyper/issues/2431), it will become more difficult to tell at a glance whether any given call is internal or external, and will require referencing the definition of a function to determine if it is internal or external. This seems to go against vyper's goal of readability/auditability. It would also help the author of a contract, as it forces them to consider the consequences every time they call to an external contract. External calls are expensive to reason about, and the syntax should reflect that! Lastly, this VIP makes it easier to find all external calls made by a contract (which might be done during code review, audit or vulnerability scanning), as it can be done with simple text search.
This VIP proposes the use of the await keyword to signal that a call is external. This keyword is already familiar to Python programmers, and fits well with await's cooperative multitasking semantics - "this will pass execution control to something else, and we may get control back after it returns".
Potential Drawbacks
A drawback of this VIP is that it introduces a code reusability concern. For instance, if HelperContract is initially implemented as an internal module, called as follows
self.helper_contract.foo()
but then later due to code size issues, needs to be factored out into a separately deployed contract, any usages like the above would need to be changed to
await self.helper_contract.foo()
This might be annoying to do. But, maybe this is a feature, not a bug(!), in that it forces the programmer to be explicit about the scope and execution context of the helper contract.
Specification
Calls to external contracts are required to be prefixed with the await keyword. If it is not (or, conversely, if a call to an internal module is prefixed with await), a semantic or typechecker error should be thrown.
Backwards Compatibility
Users will need to syntactically update every call to an external contract.
Dependencies
References
#1954, #2431
Copyright
Copyright and related rights waived via CC0
meeting notes: approved, use await keyword, wait until next "minor" release. (may reconsider the keyword in the future if actual async calls are a thing, e.g. sharded execution)
another even more exotic thought -- we could have all external fns be declared with external def, (and internal is the default visibility). this maps directly onto the python AsyncFunctionDef AST type (https://docs.python.org/3/library/ast.html#ast.AsyncFunctionDef, e.g. async def foo(): ...)
this starts to break down a bit though because in python, await is only allowed within async functions whereas in vyper, await would be allowed in any function.
Definitely don't care for 'await' being used in this regard because it strongly infers asynchronous semantics which are not present in the EVM. 'call' or 'send' (as in sending a msg to another actor/contract entity) seem closer semantically to me. I would also suggest that some variant of this could be used to represent delegate-style calls possibly but I obviously haven't thought that one through all the way.
Hello, maybe I'm a bit late to the party but I think it would be worth considering other naming possibilities rather than the await keyword. I get why this was proposed but I think it might be better to use something like ext or the previously proposed call. While I understand the need for an additional keyword I feel like await might create serious confusion.
another idea for the keyword (from kotlin) - suspend
How about messagecall?
I also dislike await for similar reasons.
I think we both want to mark the call and also make the distinction between jmp and call explicit - as such I prefer keyword variants with call within them.
I like either call or extcall. But messagecall is also fine by me.
What about just external? The fact that is a function call should be implicit and we're still making the developer aware of the difference. Otherwise ext or extcall seem reasonable as well to me.
just tried out a few different options, and msgcall, await, extcall and external decent on the screen - i think those options are a good number of characters
after trying out rewriting some test contracts, it seems that a further differentiation between extcall and staticcall is useful. from a UX perspective, extcall yells at the programmer "this needs its own line!", whereas staticcall looks idempotent (can call it multiple times without changing the result).
this was implemented in #2938