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
(每个消耗3
gas)替换为两个PUSH0
(每个消耗2
gas),减少运行时 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.