ERC-3448 Metaproxy standard

Final · Other · created 2021-03-29

Standardizes the bytecode of a tiny proxy contract with fixed configuration values (metadata) appended to the end of its code, so the same functionality can be cloned cheaply with different values and tools can easily verify that structure.

Why it is needed

When you want to deploy the same contract functionality to many addresses with only the configuration values changed, having each clone keep its own values in storage is expensive. Also, if proxy code (a contract that forwards calls to another contract) varies from one to the next, external tools such as Etherscan have to analyze each one individually to learn where it forwards calls and what values are attached. Without a fixed minimal bytecode, it is also hard to rely on a clone behaving the same as its destination contract. This standard defines one known bytecode form, which makes clone deployment cheap while letting anyone inspect and verify the attached metadata.

In depth

Bytecode specification

The metaproxy bytecode is fixed as the 54-byte code stated in the original, 363d3d373d3d3d3d60368038038091363936013d73...5af43d3d93803e603457fd5bf3, and the bytes at indices 21 to 41 (inclusive) are replaced with the 20-byte address of the master contract that holds the functionality. Arbitrary metadata can follow, and the last 32 bytes (one word) of the full bytecode must be the length of the metadata in bytes as a uint256. In other words, the overall structure is <54-byte metaproxy> <arbitrary data> <byte length of the arbitrary data>.

Call forwarding behavior

The proxy forwards every call with DELEGATECALL. It appends the metadata at the end of the bytecode to the entire call input (calldata) and passes it to the implementation contract, then returns the implementation contract's return value to the caller. If the implementation contract reverts, the revert and its payload data are passed to the caller unchanged. In the annotated bytecode, CALLDATACOPY copies the call data, CODECOPY copies the part of the code after the first 54 bytes (the metadata) after it, and then execution branches to RETURN or REVERT depending on the DELEGATECALL result.

Design goals

The original states three goals. First, store each child's immutable metadata cheaply in the bytecode instead of in storage slots. Second, lower the cost of deploying clones. Third, handle error return bubbling so that revert messages reach the caller. The deployment bytecode is PUSH1 11, CODESIZE, SUB, DUP1, PUSH1 11, RETURNDATASIZE, CODECOPY, RETURNDATASIZE, RETURN, a simple structure that returns everything except the 11 bytes of deployment code as the runtime code.

How to read the metadata (example)

The example code in the original is a suggestion and not part of the standard. Because the proxy appends the metadata after the call data, the implementation contract can read the metadata length at the position calldatasize() minus 32, then go back by that length and copy the metadata with calldatacopy and interpret it with abi.decode. Two approaches are shown as examples: getMetadataWithoutCall, which reads without a call, and getMetadataViaCall, which reads through a call to itself. Solidity structs or arbitrary custom encodings can also be used.

Using the factory and initialization

The example factory creates proxies in two ways. _metaProxyFromBytes appends the result of abi.encode(a, b, c) as metadata, and _metaProxyFromCalldata appends everything after the 4th byte of calldata as metadata. Right after deployment, a one-time setup function such as init() can optionally be called in place of a constructor. The tests cover four cases: a call with no arguments, a call with arguments, a call with a return value, and verification that the payload is passed through on revert.

Things to watch out for

Original ERC · ← ERCs