详解 ERC-7201 存储命令空间

  • RareSkills
  • 发布于 2024-12-23 15:40
  • 阅读 773

详解 ERC-7201 存储命令空间

ERC-7201(前称 EIP-7201)是一个通过称为命名空间的公共标识符将存储变量组合在一起的标准,并通过 NatSpec 注释记录这些变量组。该标准的目的是简化在升级过程中对存储变量的管理。

命名空间

命名空间是编程语言中常用的一种组织和分组相关标识符(如变量、函数、类或模块)的方法,以防止命名冲突。Solidity 本身并没有命名空间的概念,但我们可以加以模拟。在我们的例子中,我们希望将合约状态变量组合在一个命名空间中。

在 Solidity 中使用命名空间的想法并不是 ERC-7201 首次提出的;它也被钻石代理模式(ERC-2535)所利用。要理解在可升级智能合约中使用命名空间的重要性,必须明白 ERC-7201 旨在解决的问题。

继承中的问题

为了演示,我们来看看一个可升级合约,它由一个代理合约和一个通过继承父合约和子合约构建的实现合约组成。在实现合约一侧,我们有一个父合约和一个子合约,每个合约都包含一个初始插槽中的状态变量。这些实现合约的存储结构将在代理合约中复制,代理合约可以是一个 透明代理。为了简单起见,假设每个变量占用一个插槽,这意味着我们仅使用 uint256 或 bytes32 这样的变量。

一个实现合约在继承父合约和子合约之间的代理合约存储槽分配图。

问题出现在实现合约中的状态变量布局在升级期间发生变化。考虑一个场景,其中父合约需要添加一个新的状态变量。因此,存储结构将修改如下:

一个实现合约有一个状态变量并继承自父合约的两个状态变量的存储槽分配图。

这种情况带来了一个挑战:variableB 之前存在的位置,现在 variableC 将放置在这里。升级破坏了存储布局,导致新的 variableC 读取到旧的 variableB 值,这就是插槽冲突。

gap方法

OpenZeppelin 通过在其可升级合约的每个合约末尾插入一个“gap”来解决这个问题,直到 4 版本。下面,我们可以观察到 ERC20Upgradeable.sol v4.9 合约的代码。

uint256[45] private __gap 变量的代码片段

__gap 变量的大小被计算为合约始终使用 50 个可用存储槽,因此上图所示的合约有 5 个状态变量。让我们将这个概念应用到我们的例子中。

如果包含 5 个状态变量的父合约包含一个具有 45 个空槽的数组作为gap,实施(和代理)合约的存储结构将如下图所示。

一个从包含私有gap变量的父合约继承的实现合约的存储槽分配示意图。

现在,在升级的情况下,父合约有 45 个空槽可供使用。假设父合约需要添加一个新的状态变量 variableN;在这种情况下,我们只需将该变量插入到gap之前,并将gap的大小减少一个,就如下面的动画所示:

在实现合约中声明状态变量时使用私有gap变量的动画

存储槽图示,展示一个从父合约继承的实现合约使用私有gap变量。

gap为合约中插入新变量而不干扰现有功能提供了便利,充当未来添加的占位符,避免存储冲突。在使用这种方法时,建议在所有实现合约中包含一个gap。

虽然这种方法缓解了在父合约中插入变量的问题,但并没有完全解决与更改实现合约中布局相关的所有问题。例如,如果我们在当前父合约之上创建一个新的父合约,则所有下方的内容将根据新父合约中的存储变量数量下移,因此仅依赖于gap并不有效。

从祖父合约继承的插槽冲突示例

因此,找到一种调整实现合约布局而不产生插槽冲突的方法至关重要。

最佳方案是为继承链中的每个实现合约分配自己的专用存储位置。

不幸的是,Solidity 目前缺乏原生机制来实现这一点(合约中变量的命名空间)。因此,这类构造必须在 Solidity 和 YUL 的限制内实施。这可以通过使用结构体来实现。让我们回顾一下在 Solidity 中存储布局是如何工作的,以及如何建立一个基于命名空间的根布局。

基于命名空间的根布局

合约的存储布局由 Solidity 生成的总体总结如下,其中 L 表示存储中的位置,n 是自然数,H(k) 是应用于特定类型键 k 的函数,该键可以是,例如,映射键或数组的索引。

$$ L{root} := root \, \left| L{root} + n \, |\, \texttt{keccak256}(L{root}) \, |\, \texttt{keccak256}(H(k) \oplus L{root}) \right.

$$

上述公式表明可以找到状态变量:

  • 在根目录,默认是插槽 0,
  • 语法的任何元素加上一个自然数。
  • 在根据从键计算得出的确定性值的 keccak 之内,以及状态变量从根目录的位置。

我们需要认识到的是,存储布局中的所有位置都依赖于根目录。 Solidity 为任何合约指定值零作为根目录。

如果我们想为合约的变量创建自己的存储位置,则需要基于某个唯一于该合约的标签“更改”根目录。正是这个标签我们定义为合约的命名空间。

智能合约中命名空间的概念 旨在确保使用命名空间的合约的存储布局根不再位于插槽零,而是位于由选择的命名空间决定的特定插槽中

![三个命名空间示例的示意图](https://img.learnblockchain.cn/attachments/migr...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
5 订阅 19 篇文章

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/