vyper
vyper copied to clipboard
VIP: Factory contracts
Simple Summary
Create a first class factory contract, which stores initcode instead of runtime code.
Motivation
https://github.com/vyperlang/vyper/pull/2895 introduces the create_with_code_of()
factory function. It is a good mechanism for calling arbitrary initcode (e.g., factory contracts that are created in solidity). However, it is untyped; it's cumbersome to assemble the constructor args to forward to the factory contract, the constructor arguments are not typechecked, and the type of the returned contract is unknown. Constructing the deploy code for the factory is also slightly tedious (although this can be solved at the tooling level). For Vyper-generated factories, we can do better. https://github.com/vyperlang/vyper/issues/2326 would be a way of implementing init code for proxies and also contracts created using the existing create*()
functions, but it uses a non-EVM-native mechanism and can't handle immutables.
Specification
Add a @factory
decorator which is only allowed on the __init__()
function. If the factory decorator is there, the regular deploy code for the factory deploys the initcode instead of the runtime code. Users of the factory will need to import the interface to the factory contract, and then call SomeFactory.create(*args)
(where *args
are the arguments specified in SomeFactory.__init__()
) which loads the initcode into memory along with *args
, and then executes the initcode with CREATE/CREATE2.
Note that SomeFactory.create(*args)
would be equivalent to create_with_code_of(some_factory.address, *args)
, with the added compile-time check that the arguments line up with the signature of SomeFactory.__init__()
, and with the resulting address already casted to the target type of SomeFactory
. Example usage:
# foo_wizard.vy
# some contract which is a magician when it comes to `foo`
FOO: immutable(uint256)
@factory
@external
def __init__(foo: uint256):
FOO = foo
@external
def what_is_foo() -> uint256:
return FOO
# factory.vy
import foo_wizard as foo_wizard
@external
def create_foo_wizard(factory: foo_wizard.Factory, arg1: uint256) -> foo_wizard.Interface:
# factory.what_is_foo() # not allowed! raises: factory is a factory contract, not a runtime contract
# t: foo_wizard.Interface = factory.create(arg1, arg1) # not allowed! does not typecheck.
t: foo_wizard.Interface = factory.create(arg1)
assert t.what_is_foo() == arg1
# does the same thing; although signature is not type checked.
v: foo_wizard.Interface = foo_wizard.Interface(create_with_code_of(factory.address, arg1))
return t
Backwards Compatibility
No breaking changes
Dependencies
Depends on syntactic changes to import system in https://github.com/vyperlang/vyper/issues/2431
References
https://github.com/vyperlang/vyper/issues/2326
Copyright
Copyright and related rights waived via CC0
A few comments:
- I think the fields should be properties and not types e.g.
foo_wizard.factory
andfoo_wizard.interface
-
foo_wizard.factory
sounds a bit domain specific. Wonder if something likefoo_wizard.create(*args)
orfoo_wizard.__init__(*args)
might make more sense semantically (same with@factory
although I could be more convinced) - Why does
create_foo_wizard
return a tuple with the second item being an integer?
- I think the fields should be properties and not types e.g.
foo_wizard.factory
andfoo_wizard.interface
noted.
foo_wizard.factory
sounds a bit domain specific. Wonder if something likefoo_wizard.create(*args)
orfoo_wizard.__init__(*args)
might make more sense semantically (same with@factory
although I could be more convinced)
i kind of like .create()
. but we do need to let the type checker know whether we are dealing with a factory contract or a regular contract.
re. the @factory
decorator -- i think it's actually not necessary for the type checker. ~we do need~ maybe instead we can just introduce a new mode for the compiler, -f factory_bytecode
(which returns the deploy code for the factory instead of deploy code for the regular contract)
- Why does
create_foo_wizard
return a tuple with the second item being an integer?
mistake, fixed
re. the
@factory
decorator -- i think it's actually not necessary for the type checker. ~we do need~ maybe instead we can just introduce a new mode for the compiler,-f factory_bytecode
(which returns the deploy code for the factory instead of deploy code for the regular contract)
Rather not have another compiler mode. If it materially impacts the bytecode that gets generated, would rather have to have something in the source code that mirrors it. I think @factory
makes sense then.
meeting notes: punt until new module system is done, and then the spec can be more concrete
Is this solved by blueprints?
Blueprints provide the implementation, this would essentially be a better UX around blueprints (so you don't need to call create_from_blueprint directly)