Skip to content

Latest commit

 

History

History
199 lines (130 loc) · 6.73 KB

README.md

File metadata and controls

199 lines (130 loc) · 6.73 KB

Approaches to the Factory pattern for Ethereum contracts

Introduction

Deploying contracts on Ethereum networks is difficult, at least compared to interacting with already-deployed contracts.

Arguably, it requires an understanding of the employed virtual machine, and developer scaffolding around it, that normal people can't afford in terms of the most universal commodity: time.

For this reason, some Ethereum clients have attempted to ship popular contracts, such as multisig wallets, together with the client itself. This limits such contracts to a certain client implementation, locks the contract's development cycle to that of the client, and inevitably passes judgement on what it is that is "popular".

This is unacceptable!

Below find approaches to deploying contracts using other contracts; and wholly experimental implementations of these approaches in LLL. The latter was chosen for its atavistic outlook on the EVM, and what it is that is "allowed", or "possible".

No gods! No masters! No babysitters!

The tests are in ../tests/.

stamping-press (and cog, a.k.a. greeter)

The straightforward Factory. Used in Solidity programs with the new keyword.

A contract's runtime bytecode contains the deployment bytecode (and, by extension, runtime) of a yet another contract.

On receiving input, the "outer layer" contract can create (optionally parametrised) copies of this nested contract.

If the copies are to be non-identical, then the "outer layer" must know their interface: that is, it must contain code that is not strictly required for its own operation, but is dependent on the implementation of the nested contract.

Upside: simple to implement and understand.

Downside: for whatever kind of contract that one would like to be deploying, they'll need to have a separate stamping-press. If a change needs to be made to the nested contract template, then a new stamping-press will have to be deployed.

Philosophical dead-end:

Recursion is the mother of recursion.

If, instead of making objects (o), one needs to make factories that can make objects (Fo), then they can nest their desired factory in a yet another "outer layer" factory (FFo), and be done with it.

However, on the next step attempting to do the same, it becomes apparent that the third-layer indirection FFFo has the same interface (F(F..)) as the second-layer indirection FFo (F(F.)), only with a bigger nested payload.

In other words, FFFo handles its payload in the same way as FFo - they have the same basic function - yet, for some reason, they are not the same contract!

We've heard you like recursion, so...

The honeybadger Factory.

Takes an account address as input and creates a copy of that account's runtime bytecode as a new contract.

Upside: extremely simple to implement and understand.

More upsides:

No runtime? Don't care.

Runtime doesn't work without the init code? Don't care.

Even if that makes it insecure? Don't care.

Runtime makes no sense without the storage it was handling? Don't care.

Downside: needs additional steps if created contract needs customisation (such as access controls), which likely requires an additional contract that can perform creation (by use of the cloning-vat) immediately followed by initialisation, in the same externally-originating call.

By extension, this requires initialisation code to be present in the target, even if it will only be used once, and then disabled.

More downsides:

See "more upsides" above. Don't care.

Philosophically invigorating:

A cloning-vat handles just fine being given its own address as the target, and creating a perfect copy of itself. It can also be given a stamping-press as input, or any other factory for that matter.

cannery (and complementary can-opener)

The one-size-fits-all Factory. Something similar has been proposed as the copyof or shared keyword in Solidity.

The cannery takes deployment bytecode (as generated by a compiler), and stores that as code of another contract, with a protective "wrapper".

The wrapper appears at the very beginning of a can and REVERTs on any direct call to it.

To create a copy of the can's contents, a wrapper-aware can-opener has to be used, that can take the can's runtime, strip it of the REVERT guard, and then run the deployment code in a CREATE call.

Several approaches can be taken to the customisation of a can after opening it. (A primitive one is shown here: concatenating the can's contents with caller-provided data.)

Upside: compared to the cloning-vat, can run initialisation code, since the cannery gets to see it before the contract has been deployed.

Downside: every cannery should provide a "brand-specific" can-opener; an "adjustable" can-opener can be used, but only for trivial wrappers like above (i.e. "remove first N bytes").

Philosophically re-invigorating:

A cannery is essentially a cloning-vat that gets to see bytecode at its earliest stage, before it has been deployed (and its initialisation code lost in a historic block).

A further-specialised cannery may perform checks on submitted contracts - that they conform to some preordained rules; e.g. that its runtime starts with an unconditional JUMP 35, followed by 32 bytes, followed by a JUMPDEST. The 32 bytes would contain a link to the submitted contract's source code.

A complementary specialised can-opener may then refuse to open the can if it has been listed in a certain external blocklist. Alternatively, it may refuse to open the can if it's not been listed in an external clearlist. It may combine both approaches as necessary.

This is just one imaginary use case for such a "delayed deployment" pattern. Many more are certainly possible.

assembly-line (a.k.a. sequencer)

TODO: not implemented yet!

  • keeps code blocks as data in storage
  • receives blueprint/sequence
  • creates external contracts out of code blocks, concatenating them according to the blueprint/sequence

+ may be useful for one-shot "transactional" contracts

garden (and seed)

TODO: not implemented yet!

  • (optionally?) STATICCALLs into a
  • DELEGATECALL into an external contract that
  • runs its code to deploy a yet another external contract

+ pass entire storage as "call data" (2^256 2^256 bit chunks transferred for about 1000 gas? wow, that's cheap!..)

+ may be useful for "interruptable" contracts

- storage operations in general are expensive