Alert Source Discuss
Standards Track: ERC

ERC-3448: MetaProxy 标准

一个最小的字节码实现,用于创建代理合约,并将不可变的元数据附加到字节码上

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

摘要

通过标准化一个已知的、具有不可变元数据支持的最小字节码代理实现,该标准允许用户和第三方工具(例如,Etherscan)能够: (a) 简单地发现一个合约将总是以已知的方式重定向,并且 (b) 将目标合约中代码的行为视为重定向合约的行为,并且 (c) 验证/查看附加的元数据。

工具可以查询重定向地址的字节码,以确定将要运行的代码以及关联元数据的位置 - 并且可以依赖于关于该代码的表示(已验证的源代码、第三方审计等)。 此实现通过 DELEGATECALL 和任何(calldata)输入加上字节码末尾的元数据将所有调用转发到实现合约,然后将返回值转发回调用者。 如果实现发生 revert,则 revert 将与 payload 数据一起传递回去。

动机

此标准支持在另一个地址以不同的参数克隆精确的合约功能的使用场景。

规范

MetaProxy 合约的确切字节码是:

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

其中索引 21 - 41(包括)处的字节替换为主功能合约的 20 字节地址。 此外,MetaProxy 字节码之后的所有内容都可以是任意元数据,并且字节码的最后 32 个字节(一个字)必须指示元数据的长度(以字节为单位)。

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

理由

这项工作的目标如下:

  • 一种廉价的方式来为每个子合约存储不可变的元数据,而不是使用存储槽
  • 克隆的低成本部署
  • 处理 revert 消息的错误返回冒泡

向后兼容性

没有向后兼容性问题。

测试用例

已通过以下测试:

  • 无参数调用
  • 带参数调用
  • 带返回值调用
  • 带 revert 调用(确认 reverted 的 payload 已传输)

包含上述测试用例的 solidity 合约可以在 EIP 资产目录 中找到。

参考实现

参考实现可以在 EIP 资产目录 中找到。

部署字节码

一个带有注释的部署字节码版本:

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

MetaProxy

一个带有注释的 MetaProxy 字节码版本:

// 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)

示例

以下代码片段仅作为建议,并非此标准的独立部分。

使用来自 abi.encode 的字节构造代理

/// @notice 通过 abi 编码的字节构造 MetaProxy。
function createFromBytes (
  address a,
  uint256 b,
  uint256[] calldata c
) external payable returns (address proxy) {
  // 创建一个新的代理,其中元数据是 abi.encode() 的结果
  proxy = MetaProxyFactory._metaProxyFromBytes(address(this), abi.encode(a, b, c));
  require(proxy != address(0));
  // 可选的一次性设置,constructor() 的替代方法
  MyContract(proxy).init{ value: msg.value }();
}

使用来自 calldata 的字节构造代理

/// @notice 通过 calldata 构造 MetaProxy。
function createFromCalldata (
  address a,
  uint256 b,
  uint256[] calldata c
) external payable returns (address proxy) {
  // 创建一个新的代理,其中元数据是来自 calldata 的第 4 个字节之后的所有内容。
  proxy = MetaProxyFactory._metaProxyFromCalldata(address(this));
  require(proxy != address(0));
  // 可选的一次性设置,constructor() 的替代方法
  MyContract(proxy).init{ value: msg.value }();
}

从 calldata 检索元数据并进行 abi.decode

/// @notice 返回此(MetaProxy)合约的元数据。
/// 仅与通过 MetaProxy 标准创建的合约相关。
/// @dev 此函数旨在通过- & 无需调用来调用。
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)
    // 通过元数据大小 + 32 字节(长度)递增空闲内存指针
    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[]));
}

通过调用自身来检索元数据

/// @notice 返回此(MetaProxy)合约的元数据。
/// 仅与通过 MetaProxy 标准创建的合约相关。
/// @dev 此函数旨在通过调用来调用。
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)
  }
}

除了上面的例子,也可以使用 Solidity 结构体或任何自定义数据编码。

安全考虑

本标准仅涵盖字节码实现,不包括其自身的任何严重副作用。 参考实现仅作为示例。 强烈建议根据任何项目中功能的使用和实现方式来研究副作用。

版权

CC0 下放弃版权及相关权利。

Citation

Please cite this document as:

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