Alert Source Discuss
Standards Track: ERC

ERC-7201: 命名空间存储布局

命名空间存储模式中结构体存储位置的约定。

Authors Francisco Giordano (@frangio), Hadrien Croubois (@Amxx), Ernesto García (@ernestognw), Eric Lau (@ericglau)
Created 2023-06-20

摘要

我们定义 NatSpec 注解 @custom:storage-location,以记录 Solidity 或 Vyper 源代码中存储命名空间及其在存储中的位置。此外,我们定义了一个公式,用于从任意标识符派生位置。选择该公式是为了安全地防止与 Solidity 和 Vyper 使用的存储布局发生冲突。

动机

智能合约语言(如 Solidity 和 Vyper)依赖于树形存储布局。这棵树从插槽 0 开始,由连续变量的连续块组成。哈希用于确保包含映射和动态数组的值的块不发生冲突。这对于大多数合约来说已经足够了。然而,它给智能合约开发中使用的各种设计模式带来了挑战。一个例子是模块化设计,其中使用 DELEGATECALL,一个合约执行来自多个合约的代码,所有这些合约共享相同的存储空间,并且必须仔细协调如何使用它。另一个例子是可升级合约,在升级中很难添加状态变量,因为它们可能会影响预先存在变量的分配存储位置。

这些模式可以受益于跨存储空间布局状态变量,而不是使用此默认存储布局,通常是在通过哈希获得的伪随机位置。每个值可以放置在完全不同的位置,但更常见的是,一起使用的值会放入 Solidity 结构体中,并共同位于存储中。这些伪随机位置可以是新存储树的根,这些树遵循与默认存储树相同的规则。如果构建此伪随机根以使其不属于默认树的一部分,则应导致定义不相互冲突或与默认存储不冲突的独立空间。

这些存储使用模式对于 Solidity 和 Vyper 编译器是不可见的,因为它们未表示为 Solidity 状态变量。静态分析器或区块链浏览器等智能合约工具通常需要知道合约数据的存储位置。标准化存储布局的位置将允许这些工具正确解释使用这些设计模式的合约。

规范

预备知识

命名空间_由一组有序变量组成,其中一些变量可能是动态数组或映射,它们的值按照与默认存储布局相同的规则进行布局,但根位于不一定是插槽 0 的某个位置。使用命名空间来组织存储的合约被称为使用_命名空间存储

命名空间 id 是一个字符串,用于标识合约中的命名空间。它不应包含任何空格字符。

@custom:storage-location

合约中的命名空间应实现为结构体类型。这些结构体应使用 NatSpec 标记 @custom:storage-location <FORMULA_ID>:<NAMESPACE_ID> 进行注释,其中 <FORMULA_ID> 标识用于计算命名空间的根存储位置的公式,该位置基于命名空间 id。(注意:Solidity 编译器从 v0.8.20 开始在 AST 中包含此注释,因此建议将其作为使用此模式时的最低编译器版本。) 在合约外部找到的带有此注释的结构体不被视为源代码中任何合约的命名空间。

公式

erc7201 标识的公式定义为 erc7201(id: string) = keccak256(keccak256(id) - 1) & ~0xff。在 Solidity 中,这对应于表达式 keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff))。当使用此公式时,注释变为 @custom:storage-location erc7201:<NAMESPACE_ID>。例如,@custom:storage-location erc7201:foobar 注释了一个 id 为 "foobar" 的命名空间,该命名空间以 erc7201("foobar") 为根。

未来的 EIP 可能会定义带有唯一公式标识符的新公式。建议遵循此 EIP 中设置的约定,并使用 erc1234 格式的标识符。

原理

Solidity 和 Vyper 使用的树形存储布局遵循以下语法(其中 root=0):

$L_{root} := \mathit{root} \mid L_{root} + n \mid \texttt{keccak256}(L_{root}) \mid \texttt{keccak256}(H(k) \oplus L_{root}) \mid \texttt{keccak256}(L_{root} \oplus H(k))$

根的要求是它不应与属于 Solidity 和 Vyper 使用的标准存储树(root = 0)的任何存储位置重叠,也不应属于从任何其他命名空间(另一个根)派生的存储树的一部分。这样,多个命名空间可以彼此相邻以及与标准存储布局一起使用,无论是故意的还是偶然的,都不会发生冲突。公式中的术语 keccak256(id) - 1 被选择为 Solidity 未使用的位置,但它不被用作最终位置,因为命名空间可能大于 1 个插槽,并且会扩展到 keccak256(id) + n 中,这可能会被 Solidity 使用。添加了第二个哈希以防止这种情况并保证命名空间与标准存储完全不相交,假设 keccak256 抗冲突且数组不太大。

此外,命名空间位置与 256 对齐,作为潜在的优化,以预期 Verkle 状态树迁移后的 gas 计划更改,这可能会导致 256 个存储插槽的组一次全部变热。

命名

此模式有时被称为“钻石存储”。这导致它与“钻石代理模式”混淆,即使它们可以彼此独立使用。此 EIP 选择使用不同的名称,以将其与代理模式清楚地区分开来。

向后兼容性

未发现向后兼容性问题。

参考实现

pragma solidity ^0.8.20;

contract Example {
    /// @custom:storage-location erc7201:example.main
    struct MainStorage {
        uint256 x;
        uint256 y;
    }

    // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));
    bytes32 private constant MAIN_STORAGE_LOCATION =
        0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;

    function _getMainStorage() private pure returns (MainStorage storage $) {
        assembly {
            $.slot := MAIN_STORAGE_LOCATION
        }
    }

    function _getXTimesY() internal view returns (uint256) {
        MainStorage storage $ = _getMainStorage();
        return $.x * $.y;
    }
}

安全注意事项

命名空间应避免与其他命名空间或标准 Solidity 或 Vyper 存储布局发生冲突。如此处讨论的,在 keccak256 抗冲突的假设下,此 ERC 中定义的公式保证了任意命名空间 id 的此属性。

@custom:storage-location 是一个 NatSpec 注解,当前编译器不对其强制执行任何规则或赋予任何含义。合约开发人员负责实现该模式并按照注解中的声明使用命名空间。

版权

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

Citation

Please cite this document as:

Francisco Giordano (@frangio), Hadrien Croubois (@Amxx), Ernesto García (@ernestognw), Eric Lau (@ericglau), "ERC-7201: 命名空间存储布局," Ethereum Improvement Proposals, no. 7201, June 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7201.