vyper
vyper copied to clipboard
VIP: Import as Delegate
Simple Summary
Add support for DELEGATECALLing Vyper code, including type-checking.
Motivation
Sometimes it is useful to put part of the code of a module into other contracts. For example, library code that will be deployed once and used in many contracts; or modules that are too big and need to be implemented across multiple contracts. (One elaboration of this idea is the diamond pattern.) Contracts containing this extra code are typically only used for code, not storage: state access and transfers are applied to the calling contract. This is achieved with the DELEGATECALL opcode, and is a major reason why this opcode exists.
In Vyper there are two drawbacks to using DELEGATECALL:
- There is no easy syntactic support for making a call. One must use
raw_callwithis_delegate_call=True, which is more cumbersome than normal calls. - There is no type checking of these raw calls, even when the code being called is written in Vyper.
This VIP is to address both drawbacks, making it more safe and more readable to use DELEGATECALL in Vyper.
Specification
We add one additional piece of syntax, importing delegates, and one new compiler option --caller <file> for compiling delegates.
import FooLib as delegate(FooLibInterface)
is similar to
import FooLib as FooLibInterface
except that calls of functions in FooLibInterface will be treated as delegate calls. (Calls to @pure visibility functions may continue to be static calls instead of delegate calls. But @view needs to be delegated so that the correct contract's storage is read.)
For type-checking, the storage variables (storage layout + code layout) and constants of the calling contract are implicitly added to the code in FooLib.vy, to avoid unnecessary code duplication.
When compiling FooLib.vy, the option --caller Foo.vy can be added to add the storage variables etc. of Foo.vy when compiling FooLib.vy. This enables compilation of the library so that it can be deployed ahead of the main contract.
Full example:
Foo.vy:
# version ^0.3.8
import FooLib as delegate(FooLibInterface)
num: uint256
addrs: HashMap[uint256, address]
lib: immutable(FooLibInterface)
@external
def __init__(l: address, i: uint256, a: address):
self.num = i
self.addrs[i] = a
lib = FooLibInterface(l)
@external
@view
def foo() -> address:
return lib.GetAddr()
@external
def bar(k: uint256, v: address) -> address:
lib.Add(k, v)
return self.addrs[k]
FooLib.vy:
# version ^0.3.8
@external
@view
def GetAddr() -> address:
return self.addrs[self.num]
@external
def Add(key: uint256, val: address):
self.addrs[key] = val
Example deployment and use:
Compile vyper --caller Foo.vy FooLib.vy -o FooLib.code
Deploy FooLib.code at address 0xabc, with no init args
Compile vyper Foo.vy -o Foo.code
Deploy Foo.code at 0xdef, with init args 0xabc, 5, and 0x39
Call foo() on 0xdef, returns 0x39
Call GetAddr() on 0xabc returns 0x0
Call bar(7, 0x1) on 0xdef, returns 0x1
Backwards Compatibility
No incompatibilities - only additional features and syntax are proposed.
Dependencies
None.
References
TBD
Copyright
Copyright and related rights waived via CC0
The implicit code inclusion and the need for the --caller option could both be avoided with a general purpose code inclusion feature, to inline the contents of a file. (This preprocessing does not technically have to be part of Vyper itself.) In that case import as delegate should check that the imported code has the same storage layout etc as the importing code.
Typically when I do a delegate call it's because I want to catch a revert and maybe do something about it rather than just let it throw. (I guess this sort of munges raw_call with the delegate_call option.) Anyway - there's more semantics typically in a delegate call than a straight contract call in the general case for me. If that's true of others then perhaps this proposal needs to somehow anticipate & support these optional semantics. Do others find that to be the case when they use delegate_call?
Could extra semantics be left for future improvements while keeping in mind a viable extension path? E.g. try catch blocks for catching reverts. Because it would be nice to get the core call functionality in to support the code size motivation without needing to solve all uses at once. I agree it's worth thinking about to avoid a design that makes other features harder later.
cf. #3699