upgrade
在不更改合约地址的情况下进行升级?这是怎么做到的?
在以太坊智能合约中,不更改合约地址的情况下进行升级是通过代理模式实现的。代理模式中最常见的实现是 UUPS(Universal Upgradeable Proxy Standard)和 EIP-1967(一个标准化的代理存储槽)。
代理模式涉及两个关键组件:代理合约(Proxy)和实现合约(Implementation)。代理合约是用户和其他合约交互的合约地址,而实现合约包含实际的业务逻辑。这里的关键是,代理合约保持不变,但它可以指向不同的实现合约。
下面是这种模式的基本工作原理:
-
存储转发:代理合约通常不包含任何实际的业务逻辑,只是将所有的调用转发到一个实现合约。这通过在代理合约中使用一个特殊的
fallback函数或delegatecall操作来实现。delegatecall允许代理合约以自己的上下文执行另一个合约的代码,这意味着所有的状态变量实际上是在代理合约中存储的,而不是在实现合约中。 -
可升级性:实现合约可以被升级为新的版本。当需要升级时,部署一个新的实现合约,并更新代理合约中的一个特定的存储槽(通常遵循 EIP-1967 标准),指向新的实现合约地址。由于代理合约的地址保持不变,用户和其他合约不需要知道实现合约的变化。
-
透明代理:为了防止实现合约直接被调用(绕过代理合约),通常会在实现合约中加入一些保护措施,确保所有的调用都是通过代理合约进行的。
-
初始化和存储冲突:在升级过程中,需要确保新的实现合约能够与代理合约现有的存储布局兼容。此外,可能需要执行一些初始化代码来设置新合约的状态变量。
UUPS 模式是一种优化的代理模式,其中升级逻辑包含在实现合约中而非代理合约。这样做的好处是减少了所需的合约数量,以及减少部署和升级的复杂性。不过,这也意味着实现合约需要有授权升级的逻辑,通常是通过权限控制实现的。
这种升级模式的一个关键优势是它允许智能合约迭代和改进,同时保持了合约地址和其网络中的身份不变。这对于用户信任和合约接口的稳定性至关重要。然而,升级合约也增加了复杂性和潜在的安全风险,因此升级过程需要非常谨慎地进行。
代理合约(Proxy )和实现合约(Implementation)之间是怎么建立起来联系的
代理合约(Proxy)和实现合约(Implementation)之间的联系是通过特殊的存储槽建立的,这个存储槽保存了实现合约的地址。在 EIP-1967 中,定义了一些标准的存储槽位置,用于存储指向实现合约地址和其他与代理相关的数据。这样做是为了避免与代理合约自己的存储变量冲突。
以下是建立联系的步骤:
-
部署实现合约:首先,部署实现合约到区块链上。这个合约包含了所有的业务逻辑和状态变量,但它本身不会直接处理交易。在部署时,实现合约通常会被初始化,确保它处于正确的状态。
-
部署代理合约:然后,部署一个代理合约,这个合约包含了用于转发调用到实现合约的代码。代理合约在其存储中有一个固定的槽位(比如,按照 EIP-1967 的建议,使用
keccak256("eip1967.proxy.implementation") - 1计算得出的槽位),用于存储当前实现合约的地址。 -
设置实现地址:通过调用代理合约中的一个特定函数(通常是初始化函数或专门的设置函数),将实现合约的地址存储在代理合约的指定槽位中。这个步骤通常是由合约的管理员或拥有者完成的。
-
转发调用:当代理合约接收到一个函数调用时,它会使用
delegatecall操作码将这个调用转发到存储在特定槽位的实现合约地址。由于使用了delegatecall,实现合约的代码在代理合约的上下文中执行,意味着所有的状态变量都存储在代理合约中。 -
升级实现合约:如果需要升级合约,只需部署一个新的实现合约,并通过代理合约的升级函数更新存储的实现合约地址。这样,所有的新调用都会转发到新的实现合约,而代理合约的地址和存储状态保持不变。
通过这种方式,代理合约和实现合约之间建立了一个灵活的联系,可以随时通过更改一个存储槽中的地址来更换实现合约。这种模式允许智能合约系统保持灵活性和可升级性,同时为用户提供一个固定不变的合约地址。
一个合约存储的数据有上限吗?
在以太坊智能合约中,合约的存储是由键值对组成的,其中键和值都是 256 位的。理论上,以太坊合约的存储是无限的,每个合约都有足够的空间来存储任意数量的状态变量。然而,实际上,存储是有成本的,这个成本在多个层面上体现出来:
-
Gas 成本:以太坊上的每个操作都需要消耗 gas,这是一个度量单位,用来计算执行操作所需的计算资源。存储操作是非常昂贵的,特别是写入操作。首次将一个非零值写入存储槽(slot)会消耗大约 20,000 gas,而更新一个已经有值的存储槽会消耗大约 5,000 gas。因此,合约的存储成本会随着存储的增加而增加。
-
存储退款:当存储槽的值从非零值变为零时,以太坊提供了一定的 gas 退款,但这个退款有一个上限,并且不会覆盖初始写入的成本。
-
区块 gas 限制:每个以太坊区块有一个 gas 限制,这限制了可以包含在单个区块中的所有交易的总 gas 消耗。这意味着大量的存储操作可能需要分散在多个交易和区块中进行。
-
合约大小限制:以太坊也有一个合约大小的限制,这是指合约代码的大小,而不是存储的大小。EIP-170 引入了一个限制,即合约的代码大小最多为 24KB。这个限制不会影响到合约的存储大小,但会限制合约逻辑的复杂性。
因此,虽然从技术上讲合约的存储容量是无限的,但实际上,由于成本和网络资源的限制,合约的存储使用是受限的。开发者通常会寻找优化存储使用的方法,比如通过打包多个值进一个存储槽、使用事件日志来记录信息,或者通过外部存储解决方案来减少链上存储的需求。