ERC-7613: Puppet proxy contract

This is the discussion thread for PR Add EIP: Puppet proxy contract #236 . Below is the shortened version of the EIP.

Abstract

A puppet is a contract that when called, acts like an empty account, it doesn’t do anything and it has no API. The only exception is that if it’s called by the address that deployed it, it delegates the call to the address passed to it in calldata. This gives the deployer the ability to execute any logic they want in the context of the puppet.

Motivation

A puppet can be used as an alternative account of its deployer. It has a different address, so it has a separate set of asset balances. This enables sophisticated accounting, e.g. each user of a protocol can get their own address where assets can be sent and stored.

The puppet’s logic doesn’t need to be ever upgraded, to change its behavior the deployer needs to change the address it passes to the puppet to delegate to or the calldata it passes for delegation. The entire fleet of puppets deployed by a single contract can be upgraded by upgrading the contract that deployed them, without using beacons. A nice trick is that the deployer can make the puppet delegate to the address holding the deployer’s own logic, so the puppet’s logic is encapsulated in the deployer’s.

A puppet is unable to expose any API to any caller except the deployer. If a 3rd party needs to be able to somehow make the puppet execute some logic, it can’t be requested by directly calling the puppet. Instead, the deployer needs to expose a function that if called by the 3rd parties, will call the puppet, and make it execute the desired logic. Mechanisms expecting contracts to expose some APIs don’t work with puppet, e.g. ERC-721’s safeTransfers.

Because the puppet can be deployed under a predictable address despite having no fixed logic, in some cases it can be used as a CREATE3 alternative. It can be also used as a full replacement of the CREATE3 factory by using a puppet deployed using CREATE2 to deploy arbitrary code using plain CREATE.

Deploying a new puppet is almost as cheap as deploying a new clone proxy. Its whole deployed bytecode is 66 bytes, and its creation code is 62 bytes. Just like clone proxy, it can be deployed using just the Solidity scratch space in memory. The cost to deploy a puppet is 45K gas, only 4K more than a clone. Because the bytecode is not compiled, it can be reliably deployed under a predictable CREATE2 address regardless of the compiler version.

Specification

To delegate, the deployer must prepend the calldata with an ABI-encoded address to delegate to.
All the data after the address will be passed verbatim as the delegation calldata.
If the caller isn’t the deployer, the calldata is shorter than 32 bytes, or it doesn’t start with
an address left-padded with zeros, the puppet doesn’t do anything.
This lets the deployer make a plain native tokens transfer to the puppet,
it will have an empty calldata, and the puppet will accept the transfer without delegating.

The puppet is deployed bytecode and its breakdown is in the EIP PR.

Rationale

The main goals of the puppet design are low cost and modularity. It should be cheap to deploy and cheap to interact with. The contract should be self-contained, simple to reason about, and easy to use as an architectural building block.

The puppet behavior could be implemented fairly easily in Solidity with some inline Yul for delegation. This would make the bytecode much larger and more expensive to deploy. It would also be different depending on the compiler version and configuration, so deployments under predictable addresses using CREATE2 would be trickier.

A workaround for the problems with the above solution could be to use the clone proxy pattern to deploy copies of the puppet implementation. It would make the cost to deploy each puppet a little lower than deploying the bytecode proposed in this document, and the addresses of the clones would be predictable when deploying using CREATE2. The downside is that now there would be 1 extra delegation for each call, from the clone proxy to the puppet implementation address, which costs gas. The architecture of such solution is also more complicated with more contracts involved, and it requires the initialization step of deploying the puppet implementation before any clone can be deployed. The initialization step limits the CREATE2 address predictability because the creation code of the clone proxy includes the implementation address, which affects the deployment address.

Another alternative is to use the beacon proxy pattern. Making a Solidity API call safely is a relatively complex procedure that takes up a non-trivial space in the bytecode. To lower the cost of the puppets, the beacon proxy probably should be used with the clone proxy, which would be even more complicated and more expensive to use than the above solutions. Querying a beacon for the delegation address is less flexible than passing it in calldata, it requires updating the state of the beacon to change the address.

Backwards Compatibility

No backward compatibility issues found.

The puppet bytecode doesn’t use PUSH0, because many chains don’t support it yet.

Test Cases

The test cases are in the EIP PR.

Reference Implementation

The puppet bytecode is explained in the specification section. The example helper library is in the EIP PR.

Security Considerations

The bytecode is made to resemble clone proxy’s wherever it makes sense to simplify auditing.

ABI-encoding the delegation address protects the deployer from being tricked by a 3rd party into calling the puppet and making it delegate to an arbitrary address. Such scenario would only be possible if the deployer called on the puppet a function with the selector 0x00000000, which as of now doesn’t come from any reasonably named function.

Needs discussion.