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 |
Table of Contents
摘要
通过 Shanghai 升级引入的 PUSH0 操作码 (EIP-3855),我们将之前的最小代理合约 (ERC-1167) 在部署时减少了 200 gas,在运行时减少了 5 gas,同时保留了相同的功能。
动机
- 通过移除冗余的
SWAP操作码,将合约字节码大小减少1字节。 - 通过将两个
DUP(每个消耗3gas)替换为两个PUSH0(每个消耗2gas),减少运行时 gas。 - 通过使用
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 操作码。最小代理的核心要素包括:
- 使用
CALLDATACOPY复制 calldata。 - 使用
DELEGATECALL将 calldata 转发到实现合约。 - 复制来自
DELEGATECALL的返回数据。 - 根据
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,因此我们需要使用 JUMPI 和 JUMPDEST 代替。JUMPI 的参数是 [0x2a, suc],其中 0x2a 是条件跳转的目标。
我们还需要在 JUMPI 之前为 REVERT 和 RETURN 操作码准备参数 [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 替换了 RETURNDATASIZE 和 DUP 操作,这节省了 gas 并提高了代码的可读性。总而言之,新的最小代理合约在部署时减少了 200 gas,在运行时减少了 5 gas,同时保持了与旧合约相同的功能。
向后兼容性
由于新的最小代理合约使用 PUSH0 操作码,因此只能在 Shanghai 升级之后部署。它的行为与之前的最小代理合约相同。
安全注意事项
新的代理合约标准与之前的标准 (ERC-1167) 相同。以下是使用最小代理合约时的安全注意事项:
-
不可升级性:最小代理合约将其逻辑委托给另一个合约(通常称为“实现”或“逻辑”合约)。这种委托在部署时是固定的,这意味着您无法在创建后更改代理委托给哪个实现合约。
-
初始化问题:代理合约缺少构造函数,因此您需要在部署后使用初始化函数。跳过此步骤可能会使合约不安全。
-
逻辑合约的安全性:逻辑合约中的漏洞会影响所有相关的代理合约。
-
透明度问题:由于其复杂性,用户可能会将代理视为空合约,从而难以追溯到实际的逻辑合约。
版权
版权和相关权利已通过 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.