Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7760: 最小化可升级代理

具有不可变参数并支持链上实现查询的最小化可升级代理

Authors Atarpara (@Atarpara), JT Riley (@jtriley-eth), Thomas (@0xth0mas), xiaobaiskill (@xiaobaiskill), Vectorized (@Vectorized)
Created 2024-08-19
Discussion Link https://ethereum-magicians.org/t/erc-7760-minimal-upgradeable-proxies/20868
Requires EIP-1967

摘要

本标准定义了三种模式的最小化 ERC-1967 代理:(1)透明,(2)UUPS,(3)信标。 这些代理支持可选的不可变参数,这些参数附加到其运行时字节码的末尾。 提供了支持链上实现查询的附加变体。

动机

拥有用于可升级代理的标准化最小字节码可以实现以下目的:

  1. 在区块浏览器上自动验证。
  2. 能够在链上查询不可变参数,因为这些参数存储在相同的字节码偏移量处,
  3. 能够在链上查询和验证实现。

代理的最小化性质使得部署和运行时成本更低。

规范

本文档中的关键词“必须”,“禁止”,“需要”,“应该”,“不应该”,“推荐”,“不推荐”,“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

一般规范

以下所有代理都可以将可选的数据字节码附加到其运行时字节码的末尾。

在初始化期间发出 ERC-1967 事件是“可选”的。 索引器“不得”期望初始化代码发出 ERC-1967 事件。

I-变体的实现的链上查询

I-变体具有将实现烘焙到其字节码中的逻辑。

当使用任何 1 字节的 calldata 调用时,这些 I-变体将返回地址(左侧零填充到 32 字节),并且不会将 calldata 转发到目标。

在任何可选的不可变参数之前的代理字节码“必须”通过以下步骤进行验证:

  1. 使用 EXTCODECOPY 获取任何不可变参数之前的字节码。
  2. 将获取的字节码中任何内置的工厂地址归零。
  3. 确保最终获取的字节码的哈希值与字节码的预期哈希值匹配。

如果哈希值不匹配,则返回的实现地址“不得”被信任。

最小化 ERC-1967 透明可升级代理

建议透明可升级代理由一个工厂部署,该工厂兼作经过身份验证以执行升级的帐户。 外部拥有的帐户可以代表工厂执行部署。 为了方便起见,我们将工厂称为被授权调用代理上的升级逻辑的不可变帐户。

由于代理的运行时字节码包含允许工厂使用任何值设置任何存储槽的逻辑,因此初始化代码“可以”跳过存储实现槽。

升级逻辑不会发出 ERC-1967 事件。 索引器“不得”期望升级逻辑发出 ERC-1967 事件。

在升级期间,工厂“必须”使用以下 calldata 调用可升级代理:

abi.encodePacked(
    // 新的实现地址,转换为 32 字节的字。
    uint256(uint160(implementation)),
    // ERC-1967 实现槽。
    bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc),
    // 可选的 calldata,用于在设置实现槽后通过 delegatecall 转发到实现
    ""
)

最小化 ERC-1967 透明可升级代理(基本变体)

运行时字节码(20 字节工厂地址子变体):

3d3d3373________________________________________14605757363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d356020355560408036111560525736038060403d373d3d355af43d6000803e6052573d6000fd

其中 ________________________________________ 是 20 字节的工厂地址。

运行时字节码(14 字节工厂地址子变体):

3d3d336d____________________________14605157363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e604c573d6000fd5b3d6000f35b3d3560203555604080361115604c5736038060403d373d3d355af43d6000803e604c573d6000fd

其中 ____________________________ 是 14 字节的工厂地址。

最小化 ERC-1967 透明可升级代理(I-变体)

运行时字节码(20 字节工厂地址子变体):

3658146083573d3d3373________________________________________14605D57363d3d37363D7f360894a13ba1A3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6058573d6000fd5b3d6000f35b3d35602035556040360380156058578060403d373d3d355af43d6000803e6058573d6000fd5b602060293d393d51543d52593df3

其中 ________________________________________ 是 20 字节的工厂地址。

运行时字节码(14 字节工厂地址子变体):

365814607d573d3d336d____________________________14605757363d3D37363d7F360894A13Ba1A3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d35602035556040360380156052578060403d373d3d355af43d6000803e6052573d6000fd5b602060233d393d51543d52593df3

其中 ____________________________ 是 14 字节的工厂地址。

最小化 ERC-1967 UUPS 代理

由于此代理不包含升级逻辑,因此初始化代码“必须”将实现存储在 ERC-1967 实现存储槽 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 处。

最小化 ERC-1967 UUPS 代理(基本变体)

运行时字节码:

363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3

最小化 ERC-1967 UUPS 代理(I-变体)

运行时字节码:

365814604357363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e603e573d6000fd5b3d6000f35b6020600f3d393d51543d52593df3

最小化 ERC-1967 信标代理

由于此代理不包含升级逻辑,因此初始化代码“必须”将实现存储在 ERC-1967 实现存储槽 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 处。

最小化 ERC-1967 信标代理(基本变体)

运行时字节码:

363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa5036515af43d6000803e604d573d6000fd5b3d6000f3

最小化 ERC-1967 信标代理(I-变体)

运行时字节码:

363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa361460525736515af43d600060013e6052573d6001fd5b3d6001f3

理由

不使用 PUSH0 操作码

为了更广泛的 EVM 兼容性,代理特意不使用 EIP-3855 中提议的 PUSH0 操作码。

将代理转换为 PUSH0 变体可能会在未来的单独 ERC 中完成。

优化优先级

代理首先针对最小化运行时 gas 进行优化,然后才是最小化字节码大小。

最小化性质

这些代理由手工制作的 EVM 字节码制成。 虽然在开发时已尽最大努力确保它们尽可能小,但它们有可能得到进一步优化。 如果某个变体已经在实际中使用,则最好在本标准中保留其现有布局,因为自动区块浏览器验证的好处将超过运行时或部署期间节省的少量 gas。

作为历史参考,ERC-1167 最小代理在编写时并非理论上的最小代理。 0age 最小代理具有更低的运行时 gas 成本和更小的字节码大小。

透明可升级代理

透明可升级代理中的工厂地址被烘焙到最小透明可升级代理的不可变字节码中。

这是为了为每个代理调用节省一个 SLOAD

由于工厂可以包含允许管理员轮换的自定义授权逻辑,因此我们不会失去任何灵活性。

升级逻辑采用任何 32 字节的值和 32 字节的存储槽。 这是为了灵活性和字节码简洁性。

由于实现仍然可以修改任何存储槽,因此我们不会失去任何安全性。

14 字节工厂地址子变体

将透明可升级代理工厂安装在具有前导零字节的虚地址处是有益的,这样可以优化代理的字节码以使其更短。

之所以选择 14 字节的工厂地址(即 6 个前导零字节),是因为它在挖掘成本和字节码大小之间取得了平衡。

I-变体

所谓的“I-变体”包含返回烘焙到代理字节码中的实现地址的逻辑。

这允许合约以可验证的方式在链上检索代理的实现。

只要代理的运行时字节码以本标准中的字节码开头,我们就可以确定实现地址不会被欺骗。

选择保留 1 字节的 calldata 来表示实现查询请求是为了提高效率并防止 calldata 冲突。 常规 ETH 转移使用 0 字节的 calldata,而常规 Solidity 函数调用使用 4 字节或更长的 calldata。

字节码中省略事件

这是为了最小化字节码大小和部署成本。

大多数区块浏览器和索引器都能够仅通过读取槽来推断最新的实现,而无需使用事件。

不可变参数不附加到转发的 calldata

这是为了避免与其他将额外数据附加到 calldata 的 ERC 标准的兼容性和安全问题。

EXTCODECOPY 操作码可用于检索不可变参数。

没有固定的初始化代码

只要初始化代码能够初始化所需的 ERC-1967 实现槽(即,对于 UUPS 代理和信标代理),就不需要对初始化代码提出其他要求。

超出范围的主题

以下主题有意超出本标准的范围,因为它们可以包含自定义逻辑:

  • 用于代理部署的工厂。
  • 用于在链上从 I-变体读取和验证实现的逻辑。
  • 用于信标代理的信标。

然而,它们需要仔细实施以确保安全性和正确性。

向后兼容性

未发现向后兼容性问题。

参考实现

最小化 ERC-1967 透明可升级代理实现

最小化 ERC-1967 透明可升级代理实现(基本变体)

pragma solidity ^0.8.0;

library ERC1967MinimalTransparentUpgradeableProxyLib {
    function initCodeFor20ByteFactoryAddress() internal view returns (bytes memory) {
        return abi.encodePacked(
            bytes13(0x607f3d8160093d39f33d3d3373),
            address(this),
            bytes32(0x14605757363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc),
            bytes32(0x3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b),
            bytes32(0x3d356020355560408036111560525736038060403d373d3d355af43d6000803e),
            bytes7(0x6052573d6000fd)
        );
    }

    function initCodeFor14ByteFactoryAddress() internal view returns (bytes memory) {
        return abi.encodePacked(
            bytes13(0x60793d8160093d39f33d3d336d),
            uint112(uint160(address(this))),
            bytes32(0x14605157363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc),
            bytes32(0x3735a920a3ca505d382bbc545af43d6000803e604c573d6000fd5b3d6000f35b),
            bytes32(0x3d3560203555604080361115604c5736038060403d373d3d355af43d6000803e),
            bytes7(0x604c573d6000fd)
        );
    }

    function initCode() internal view returns (bytes memory) {
        if (uint160(address(this)) >> 112 != 0) {
            return initCodeFor20ByteFactoryAddress();
        } else {
            return initCodeFor14ByteFactoryAddress();
        }
    }

    function deploy(address implementation, bytes memory initializationData)
        internal
        returns (address instance)
    {
        bytes memory m = initCode();
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed."); // 部署失败。
        upgrade(instance, implementation, initializationData);
    }

    function upgrade(address instance, address implementation, bytes memory upgradeData) internal {
        (bool success,) = instance.call(
            abi.encodePacked(
                // The new implementation address, converted to a 32-byte word.
                uint256(uint160(implementation)),
                // ERC-1967 implementation slot.
                bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc),
                // Optional calldata to be forwarded to the implementation
                // via delegatecall after setting the implementation slot.
                upgradeData
            )
        );
        require(success, "Upgrade failed.");  // 升级失败。
    }
}

最小化 ERC-1967 透明可升级代理实现(I-变体)

pragma solidity ^0.8.0;

library ERC1967IMinimalTransparentUpgradeableProxyLib {
    function initCodeFor20ByteFactoryAddress() internal view returns (bytes memory) {
        return abi.encodePacked(
            bytes19(0x60923d8160093d39f33658146083573d3d3373),
            address(this),
            bytes20(0x14605D57363d3d37363D7f360894a13ba1A32106),
            bytes32(0x67c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e60),
            bytes32(0x58573d6000fd5b3d6000f35b3d35602035556040360380156058578060403d37),
            bytes32(0x3d3d355af43d6000803e6058573d6000fd5b602060293d393d51543d52593df3)
        );
    }

    function initCodeFor14ByteFactoryAddress() internal view returns (bytes memory) {
        return abi.encodePacked(
            bytes19(0x608c3d8160093d39f3365814607d573d3d336d),
            uint112(uint160(address(this))),
            bytes20(0x14605757363d3D37363d7F360894A13Ba1A32106),
            bytes32(0x67c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e60),
            bytes32(0x52573d6000fd5b3d6000f35b3d35602035556040360380156052578060403d37),
            bytes32(0x3d3d355af43d6000803e6052573d6000fd5b602060233d393d51543d52593df3)
        );
    }

    function initCode() internal view returns (bytes memory) {
        if (uint160(address(this)) >> 112 != 0) {
            return initCodeFor20ByteFactoryAddress();
        } else {
            return initCodeFor14ByteFactoryAddress();
        }
    }

    function deploy(address implementation, bytes memory initializationData)
        internal
        returns (address instance)
    {
        bytes memory m = initCode();
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed."); // 部署失败。
        upgrade(instance, implementation, initializationData);
    }

    function upgrade(address instance, address implementation, bytes memory upgradeData) internal {
        (bool success,) = instance.call(
            abi.encodePacked(
                // The new implementation address, converted to a 32-byte word.
                uint256(uint160(implementation)),
                // ERC-1967 implementation slot.
                bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc),
                // Optional calldata to be forwarded to the implementation
                // via delegatecall after setting the implementation slot.
                upgradeData
            )
        );
        require(success, "Upgrade failed."); // 升级失败。
    }
}

最小化 ERC-1967 UUPS 代理实现

最小化 ERC-1967 UUPS 代理实现(基本变体)

pragma solidity ^0.8.0;

library ERC1967MinimalUUPSProxyLib {
    function initCode(address implementation, bytes memory args)
        internal
        pure
        returns (bytes memory)
    {
        uint256 n = 0x003d + args.length;
        require(n <= 0xffff, "Immutable args too long."); // 不可变参数太长。
        return abi.encodePacked(
            bytes1(0x61),
            uint16(n),
            bytes7(0x3d8160233d3973),
            implementation,
            bytes2(0x6009),
            bytes32(0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076),
            bytes32(0xcc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3),
            args
        );
    }

    function deploy(address implementation, bytes memory args)
        internal
        returns (address instance)
    {
        bytes memory m = initCode(implementation, args);
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed."); // 部署失败。
    }
}

最小化 ERC-1967 UUPS 代理实现(I-变体)

pragma solidity ^0.8.0;

library ERC1967IMinimalUUPSProxyLib {
    function initCode(address implementation, bytes memory args)
        internal
        pure
        returns (bytes memory)
    {
        uint256 n = 0x0052 + args.length;
        require(n <= 0xffff, "Immutable args too long."); // 不可变参数太长。
        return abi.encodePacked(
            bytes1(0x61),
            uint16(n),
            bytes7(0x3d8160233d3973),
            implementation,
            bytes23(0x600f5155f3365814604357363d3d373d3d363d7f360894),
            bytes32(0xa13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af4),
            bytes32(0x3d6000803e603e573d6000fd5b3d6000f35b6020600f3d393d51543d52593df3),
            args
        );
    }

    function deploy(address implementation, bytes memory args)
        internal
        returns (address instance)
    {
        bytes memory m = initCode(implementation, args);
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed."); // 部署失败。
    }
}

最小化 ERC-1967 信标代理实现

最小化 ERC-1967 信标代理实现(基本变体)

pragma solidity ^0.8.0;

library ERC1967MinimalBeaconProxyLib {
    function initCode(address beacon, bytes memory args) internal pure returns (bytes memory) {
        uint256 n = 0x0052 + args.length;
        require(n <= 0xffff, "Immutable args too long."); // 不可变参数太长。
        return abi.encodePacked(
            bytes1(0x61),
            uint16(n),
            bytes7(0x3d8160233d3973),
            beacon,
            bytes23(0x60195155f3363d3d373d3d363d602036600436635c60da),
            bytes32(0x1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6c),
            bytes32(0xb3582b35133d50545afa5036515af43d6000803e604d573d6000fd5b3d6000f3),
            args
        );
    }

    function deploy(address beacon, bytes memory args) internal returns (address instance) {
        bytes memory m = initCode(beacon, args);
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed."); // 部署失败。
    }
}

最小化 ERC-1967 信标代理实现(I-变体)

pragma solidity ^0.8.0;

library ERC1967IMinimalBeaconProxyLib {
    function initCode(address beacon, bytes memory args) internal pure returns (bytes memory) {
        uint256 n = 0x0057 + args.length;
        require(n <= 0xffff, "Immutable args too long."); // 不可变参数太长。
        return abi.encodePacked(
            bytes1(0x61),
            uint16(n),
            bytes7(0x3d8160233d3973),
            beacon,
            bytes28(0x60195155f3363d3d373d3d363d602036600436635c60da1b60e01b36),
            bytes32(0x527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b3513),
            bytes32(0x3d50545afa361460525736515af43d600060013e6052573d6001fd5b3d6001f3),
            args
        );
    }

    function deploy(address beacon, bytes memory args) internal returns (address instance) {
        bytes memory m = initCode(beacon, args);
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed."); // 部署失败。
    }
}

安全考虑

透明可升级代理工厂安全注意事项

为确保安全,透明可升级代理工厂必须实施适当的访问控制,以允许仅由授权帐户升级代理。

I-变体的 Calldata 长度冲突

I-变体保留长度为 1 的所有 calldata,以表示返回实现的请求。 如果底层实现实际上将 1 字节的 calldata 用于特殊目的,这可能会导致兼容性问题。

版权

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

Citation

Please cite this document as:

Atarpara (@Atarpara), JT Riley (@jtriley-eth), Thomas (@0xth0mas), xiaobaiskill (@xiaobaiskill), Vectorized (@Vectorized), "ERC-7760: 最小化可升级代理 [DRAFT]," Ethereum Improvement Proposals, no. 7760, August 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7760.