Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7511: 使用 PUSH0 的最小代理合约

使用 PUSH0 操作码优化之前的最小代理合约

Authors 0xAA (@AmazingAng), vectorized (@Vectorized), 0age (@0age)
Created 2023-09-04
Discussion Link https://ethereum-magicians.org/t/erc-7511-minimal-proxy-contract-with-push0/15662
Requires EIP-7, EIP-211, EIP-1167, EIP-3855

摘要

通过 Shanghai 升级引入的 PUSH0 操作码 (EIP-3855),我们将之前的最小代理合约 (ERC-1167) 在部署时减少了 200 gas,在运行时减少了 5 gas,同时保留了相同的功能。

动机

  1. 通过移除冗余的 SWAP 操作码,将合约字节码大小减少 1 字节。
  2. 通过将两个 DUP(每个消耗 3 gas)替换为两个 PUSH0(每个消耗 2 gas),减少运行时 gas。
  3. 通过使用 PUSH0 从第一性原理重新设计代理合约,提高其可读性。

规范

标准代理合约

使用 PUSH0 的最小代理合约的确切运行时代码是:

365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3

其中索引 9 - 28(包括)处的字节被替换为主实现合约的 20 字节地址。运行时代码的长度为 44 字节。

新的最小代理合约代码的反汇编是:

pc op opcode stack
[00] 36 CALLDATASIZE cds
[01] 5f PUSH0 0 cds
[02] 5f PUSH0 0 0 cds
[03] 37 CALLDATACOPY  
[04] 5f PUSH0 0
[05] 5f PUSH0 0 0
[06] 36 CALLDATASIZE cds 0 0
[07] 5f PUSH0 0 cds 0 0
[08] 73bebe. PUSH20 0xbebe. 0xbebe. 0 cds 0 0
[1d] 5a GAS gas 0xbebe. 0 cds 0 0
[1e] f4 DELEGATECALL suc
[1f] 3d RETURNDATASIZE rds suc
[20] 5f PUSH0 0 rds suc
[21] 5f PUSH0 0 0 rds suc
[22] 3e RETURNDATACOPY suc
[23] 5f PUSH0 0 suc
[24] 3d RETURNDATASIZE rds 0 suc
[25] 91 SWAP2 suc 0 rds
[26] 602a PUSH1 0x2a 0x2a suc 0 rds
[27] 57 JUMPI 0 rds
[29] fd REVERT  
[2a] 5b JUMPDEST 0 rds
[2b] f3 RETURN  

最小创建代码

最小代理合约的最小创建代码是:

602c8060095f395ff3365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3

其中前 9 个字节是 initcode:

602c8060095f395ff3

其余的是代理的运行时/合约代码。创建代码的长度为 53 字节。

使用 Solidity 部署

可以使用以下合约通过 Solidity 部署最小代理合约:

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

// Note: this contract requires `PUSH0`, which is available in solidity > 0.8.20 and EVM version > Shanghai
// 注意:此合约需要 `PUSH0`,它在 solidity > 0.8.20 和 EVM 版本 > Shanghai 中可用
contract Clone0Factory {
    error FailedCreateClone();

    receive() external payable {}

    /**
     * @dev Deploys and returns the address of a clone0 (Minimal Proxy Contract with `PUSH0`) that mimics the behaviour of `implementation`.
     *
     * This function uses the create opcode, which should never revert.
     * @dev 部署并返回 clone0(带有 `PUSH0` 的最小代理合约)的地址,该地址模仿 `implementation` 的行为。
     *
     * 此函数使用create操作码,该操作码不应恢复。
     */
    function clone0(address impl) public payable returns (address addr) {
        // first 18 bytes of the creation code
        // 创建代码的前 18 个字节
        bytes memory data1 = hex"602c8060095f395ff3365f5f375f5f365f73";
        // last 15 bytes of the creation code
        // 创建代码的最后 15 个字节
        bytes memory data2 = hex"5af43d5f5f3e5f3d91602a57fd5bf3";
        // complete the creation code of Clone0
        // 完成 Clone0 的创建代码
        bytes memory _code = abi.encodePacked(data1, impl, data2);

        // deploy with create op
        // 使用 create op 部署
        assembly {
            // create(v, p, n)
            addr := create(callvalue(), add(_code, 0x20), mload(_code))
        }

        if (addr == address(0)) {
            revert FailedCreateClone();
        }
    }
}

理由

优化的合约由代理合约的基本组件构成,并包含最近添加的 PUSH0 操作码。最小代理的核心要素包括:

  1. 使用 CALLDATACOPY 复制 calldata。
  2. 使用 DELEGATECALL 将 calldata 转发到实现合约。
  3. 复制来自 DELEGATECALL 的返回数据。
  4. 根据 DELEGATECALL 是否成功,返回结果或恢复事务。

步骤 1:复制 Calldata

要复制 calldata,我们需要为 CALLDATACOPY 操作码提供参数,即 [0, 0, cds],其中 cds 表示 calldata 大小。

pc op opcode stack
[00] 36 CALLDATASIZE cds
[01] 5f PUSH0 0 cds
[02] 5f PUSH0 0 0 cds
[03] 37 CALLDATACOPY  

步骤 2:Delegatecall

要将 calldata 转发到委托调用,我们需要为 DELEGATECALL 操作码准备参数,即 [gas 0xbebe. 0 cds 0 0],其中 gas 表示剩余 gas,0xbebe. 表示实现合约的地址,suc 表示委托调用是否成功。

pc op opcode stack
[04] 5f PUSH0 0
[05] 5f PUSH0 0 0
[06] 36 CALLDATASIZE cds 0 0
[07] 5f PUSH0 0 cds 0 0
[08] 73bebe. PUSH20 0xbebe. 0xbebe. 0 cds 0 0
[1d] 5a GAS gas 0xbebe. 0 cds 0 0
[1e] f4 DELEGATECALL suc

步骤 3:复制来自 DELEGATECALL 的返回数据

要复制 returndata,我们需要为 RETURNDATACOPY 操作码提供参数,即 [0, 0, red],其中 rds 表示来自 DELEGATECALL 的 returndata 的大小。

pc op opcode stack
[1f] 3d RETURNDATASIZE rds suc
[20] 5f PUSH0 0 rds suc
[21] 5f PUSH0 0 0 rds suc
[22] 3e RETURNDATACOPY suc

步骤 4:返回或恢复

最后,我们需要根据 DELEGATECALL 是否成功来返回数据或恢复事务。操作码中没有 if/else,因此我们需要使用 JUMPIJUMPDEST 代替。JUMPI 的参数是 [0x2a, suc],其中 0x2a 是条件跳转的目标。

我们还需要在 JUMPI 之前为 REVERTRETURN 操作码准备参数 [0, rds] ,否则我们需要准备两次。我们无法避免 SWAP 操作,因为我们只能在 DELEGATECALL 之后获得 rds

pc op opcode stack
[23] 5f PUSH0 0 suc
[24] 3d RETURNDATASIZE rds 0 suc
[25] 91 SWAP2 suc 0 rds
[26] 602a PUSH1 0x2a 0x2a suc 0 rds
[27] 57 JUMPI 0 rds
[29] fd REVERT  
[2a] 5b JUMPDEST 0 rds
[2b] f3 RETURN  

最后,我们得到了使用 PUSH0 的最小代理合约的运行时代码:

365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3

运行时代码的长度为 44 字节,比之前的最小代理合约减少了 1 字节。此外,它用 PUSH0 替换了 RETURNDATASIZEDUP 操作,这节省了 gas 并提高了代码的可读性。总而言之,新的最小代理合约在部署时减少了 200 gas,在运行时减少了 5 gas,同时保持了与旧合约相同的功能。

向后兼容性

由于新的最小代理合约使用 PUSH0 操作码,因此只能在 Shanghai 升级之后部署。它的行为与之前的最小代理合约相同。

安全注意事项

新的代理合约标准与之前的标准 (ERC-1167) 相同。以下是使用最小代理合约时的安全注意事项:

  1. 不可升级性:最小代理合约将其逻辑委托给另一个合约(通常称为“实现”或“逻辑”合约)。这种委托在部署时是固定的,这意味着您无法在创建后更改代理委托给哪个实现合约。

  2. 初始化问题:代理合约缺少构造函数,因此您需要在部署后使用初始化函数。跳过此步骤可能会使合约不安全。

  3. 逻辑合约的安全性:逻辑合约中的漏洞会影响所有相关的代理合约。

  4. 透明度问题:由于其复杂性,用户可能会将代理视为空合约,从而难以追溯到实际的逻辑合约。

版权

版权和相关权利已通过 CC0 放弃。

Citation

Please cite this document as:

0xAA (@AmazingAng), vectorized (@Vectorized), 0age (@0age), "ERC-7511: 使用 PUSH0 的最小代理合约 [DRAFT]," Ethereum Improvement Proposals, no. 7511, September 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7511.