Alert Source Discuss
Standards Track: ERC

ERC-3448: MetaProxy Standard

A minimal bytecode implementation for creating proxy contracts with immutable metadata attached to the bytecode

Authors pinkiebell (@pinkiebell)
Created 2021-03-29

Abstract

By standardizing on a known minimal bytecode proxy implementation with support for immutable metadata, this standard allows users and third party tools (e.g. Etherscan) to: (a) simply discover that a contract will always redirect in a known manner and (b) depend on the behavior of the code at the destination contract as the behavior of the redirecting contract and (c) verify/view the attached metadata.

Tooling can interrogate the bytecode at a redirecting address to determine the location of the code that will run along with the associated metadata - and can depend on representations about that code (verified source, third-party audits, etc). This implementation forwards all calls via DELEGATECALL and any (calldata) input plus the metadata at the end of the bytecode to the implementation contract and then relays the return value back to the caller. In the case where the implementation reverts, the revert is passed back along with the payload data.

Motivation

This standard supports use-cases wherein it is desirable to clone exact contract functionality with different parameters at another address.

Specification

The exact bytecode of the MetaProxy contract is:

                                              20 bytes target contract address
                                          ----------------------------------------
363d3d373d3d3d3d60368038038091363936013d7300000000000000000000000000000000000000005af43d3d93803e603457fd5bf3

wherein the bytes at indices 21 - 41 (inclusive) are replaced with the 20 byte address of the master functionality contract. Additionally, everything after the MetaProxy bytecode can be arbitrary metadata and the last 32 bytes (one word) of the bytecode must indicate the length of the metadata in bytes.

<54 bytes metaproxy> <arbitrary data> <length in bytes of arbitrary data (uint256)>

Rationale

The goals of this effort have been the following:

  • a cheap way of storing immutable metadata for each child instead of using storage slots
  • inexpensive deployment of clones
  • handles error return bubbling for revert messages

Backwards Compatibility

There are no backwards compatibility issues.

Test Cases

Tested with:

  • invocation with no arguments
  • invocation with arguments
  • invocation with return values
  • invocation with revert (confirming reverted payload is transferred)

A solidity contract with the above test cases can be found in the EIP asset directory.

Reference Implementation

A reference implementation can be found in the EIP asset directory.

Deployment bytecode

A annotated version of the deploy bytecode:

// PUSH1 11;
// CODESIZE;
// SUB;
// DUP1;
// PUSH1 11;
// RETURNDATASIZE;
// CODECOPY;
// RETURNDATASIZE;
// RETURN;

MetaProxy

A annotated version of the MetaProxy bytecode:

// copy args
// CALLDATASIZE;   calldatasize
// RETURNDATASIZE; 0, calldatasize
// RETURNDATASIZE; 0, 0, calldatasize
// CALLDATACOPY;

// RETURNDATASIZE; 0
// RETURNDATASIZE; 0, 0
// RETURNDATASIZE; 0, 0, 0
// RETURNDATASIZE; 0, 0, 0, 0

// PUSH1 54;       54, 0, 0, 0, 0
// DUP1;           54, 54, 0, 0, 0, 0
// CODESIZE;       codesize, 54, 54, 0, 0, 0, 0
// SUB;            codesize-54, 54, 0, 0, 0, 0
// DUP1;           codesize-54, codesize-54, 54, 0, 0, 0, 0
// SWAP2;          54, codesize-54, codesize-54, 0, 0, 0, 0
// CALLDATASIZE;   calldatasize, 54, codesize-54, codesize-54, 0, 0, 0, 0
// CODECOPY;       codesize-54, 0, 0, 0, 0

// CALLDATASIZE;   calldatasize, codesize-54, 0, 0, 0, 0
// ADD;            calldatasize+codesize-54, 0, 0, 0, 0
// RETURNDATASIZE; 0, calldatasize+codesize-54, 0, 0, 0, 0
// PUSH20 0;       addr, 0, calldatasize+codesize-54, 0, 0, 0, 0 - zero is replaced with shl(96, address())
// GAS;            gas, addr, 0, calldatasize+codesize-54, 0, 0, 0, 0
// DELEGATECALL;   (gas, addr, 0, calldatasize() + metadata, 0, 0) delegatecall to the target contract;
//
// RETURNDATASIZE; returndatasize, retcode, 0, 0
// RETURNDATASIZE; returndatasize, returndatasize, retcode, 0, 0
// SWAP4;          0, returndatasize, retcode, 0, returndatasize
// DUP1;           0, 0, returndatasize, retcode, 0, returndatasize
// RETURNDATACOPY; (0, 0, returndatasize) - Copy everything into memory that the call returned

// stack = retcode, 0, returndatasize # this is for either revert(0, returndatasize()) or return (0, returndatasize())

// PUSH1 _SUCCESS_; push jumpdest of _SUCCESS_
// JUMPI;          jump if delegatecall returned `1`
// REVERT;         (0, returndatasize()) if delegatecall returned `0`
// JUMPDEST _SUCCESS_;
// RETURN;         (0, returndatasize()) if delegatecall returned non-zero (1)

Examples

The following code snippets serve only as suggestions and are not a discrete part of this standard.

Proxy construction with bytes from abi.encode

/// @notice MetaProxy construction via abi encoded bytes.
function createFromBytes (
  address a,
  uint256 b,
  uint256[] calldata c
) external payable returns (address proxy) {
  // creates a new proxy where the metadata is the result of abi.encode()
  proxy = MetaProxyFactory._metaProxyFromBytes(address(this), abi.encode(a, b, c));
  require(proxy != address(0));
  // optional one-time setup, a constructor() substitute
  MyContract(proxy).init{ value: msg.value }();
}

Proxy construction with bytes from calldata

/// @notice MetaProxy construction via calldata.
function createFromCalldata (
  address a,
  uint256 b,
  uint256[] calldata c
) external payable returns (address proxy) {
  // creates a new proxy where the metadata is everything after the 4th byte from calldata.
  proxy = MetaProxyFactory._metaProxyFromCalldata(address(this));
  require(proxy != address(0));
  // optional one-time setup, a constructor() substitute
  MyContract(proxy).init{ value: msg.value }();
}

Retrieving the metadata from calldata and abi.decode

/// @notice Returns the metadata of this (MetaProxy) contract.
/// Only relevant with contracts created via the MetaProxy standard.
/// @dev This function is aimed to be invoked with- & without a call.
function getMetadataWithoutCall () public pure returns (
  address a,
  uint256 b,
  uint256[] memory c
) {
  bytes memory data;
  assembly {
    let posOfMetadataSize := sub(calldatasize(), 32)
    let size := calldataload(posOfMetadataSize)
    let dataPtr := sub(posOfMetadataSize, size)
    data := mload(64)
    // increment free memory pointer by metadata size + 32 bytes (length)
    mstore(64, add(data, add(size, 32)))
    mstore(data, size)
    let memPtr := add(data, 32)
    calldatacopy(memPtr, dataPtr, size)
  }
  return abi.decode(data, (address, uint256, uint256[]));
}

Retrieving the metadata via a call to self

/// @notice Returns the metadata of this (MetaProxy) contract.
/// Only relevant with contracts created via the MetaProxy standard.
/// @dev This function is aimed to to be invoked via a call.
function getMetadataViaCall () public pure returns (
  address a,
  uint256 b,
  uint256[] memory c
) {
  assembly {
    let posOfMetadataSize := sub(calldatasize(), 32)
    let size := calldataload(posOfMetadataSize)
    let dataPtr := sub(posOfMetadataSize, size)
    calldatacopy(0, dataPtr, size)
    return(0, size)
  }
}

Apart from the examples above, it is also possible to use Solidity Structures or any custom data encoding.

Security Considerations

This standard only covers the bytecode implementation and does not include any serious side effects of itself. The reference implementation only serves as a example. It is highly recommended to research side effects depending on how the functionality is used and implemented in any project.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

pinkiebell (@pinkiebell), "ERC-3448: MetaProxy Standard," Ethereum Improvement Proposals, no. 3448, March 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3448.