在这篇博客中,我们将学习开发可升级智能合约背后的基本设计原则。最后,你会更清楚为什么要升级智能合约,如何升级智能合约,以及升级时需要考虑的问题。本文章主要关注以太坊和基于 EVM 的智能合约。
为了配合学习文章内容,你应该对区块链的工作原理有初步的了解,尤其是以太坊区块链。本文后面有一个简短的代码演示,因此你最好拥有一些编程经验,对 Solidity 及其编译方式、[智能合约是什么](https://chain.link/education/smart-contracts#:~:text=A smart contract is a,certain predefined conditions are satisfied.)以及部署方式有一些基本了解,以及如何使用像是 Metamask 和 Hardhat 这些工具。
什么是可升级智能合约?
数据的不可篡改性是区块链技术的核心原则之一。存储在以太坊区块链上的数据,包括部署到它的智能合约,也是不可变的。
在我们深入了解如何升级智能合约的细节之前,让我们先明确为什么我们需要升级智能合约。
主要原因是:
- 修复 bug。
- 改进功能。
- 删掉没用的或添加需要的函数。
- 优化代码以节省更多的 gas。
- 响应技术、市场或社会的变化。
- 避免将用户迁移到新版本的成本。
如果有足够的时间,大多数东西都需要一些维护工作。但是存储在区块链上的数据是不可变的。那么智能合约如何升级呢?
简单的说,智能合约本身无法更改——一旦部署到区块链,它们就是不可变的。但是 dApp 在设计模式上,可以有一个或多个智能合约一起运行,其中一些智能合约可以作为“后端”。这样的话,我们可以升级这些智能合约之间的交互模式。在这里,升级智能合约并不意味着修改已部署的智能合约的代码,而是将其中一个智能合约换成另一个。我们这样做的方式(在大多数情况下)可以让终端用户不必改变他们与 dApp 的交互方式。
所以真正升级智能合约是一个新智能合约替换旧智能合约的过程。当新的智能合约被使用后,旧的智能合约就会被“遗弃”在链上,因为旧的合约是不可变的。
如何升级合约?
智能合约通常使用“代理模式”进行升级,“代理模式”是一种软件架构模式。可以参考这个系统设计入门第 5 节了解更多“代理模式”的细节,但是长话短说,代理可以认为是一个大软件系统中的一个软件,它代表系统的一部分。在传统的 Web2 框架中,代理位于客户端应用程序和服务器应用程序之间。其中正向代理是客户端应用程序,而反向代理是服务器应用程序。
在智能合约的架构中,代理更像是一个反向代理,代表一个智能合约。它是一种中间件,可将前端接受的请求发送给系统后端对应的智能合约。作为智能合约,代理有自己的“稳定”(即不变)的以太坊合约地址。因此,你可以把系统中的旧的智能合约替换为新部署的智能合约。dApp 的最终用户直接与代理交互,并且仅通过代理间接与其他智能合约交互。
所以,在智能合约开发中,代理模式是通过以下两个部分来实现的:
- 代理智能合约
- 执行合约,也称为逻辑合约或实现合约(implementation contract)。 在这篇文章中,我们将以上部分分别称为代理合约和逻辑合约。
代理模式有三种常见的变体,我们将在下面讨论。
简单代理模式
简单代理模式的架构如下
让我们更深入地了解它是如何工作的。
在 EVM 中,有一种叫做“execution context”的东西,你可以将其视为执行代码的所需要的空间。
代理合约有自己的 execution context,所有其他智能合约也是如此。代理合约也有自己的存储空间,数据连同自己的 ETH 余额永久存储在区块链上。智能合约相关的的数据和余额一起称为其“状态”,而状态是其 execution context 的一部分。
代理合约使用存储变量来跟踪构成 dApp 的其他智能合约的地址。这就是它可以重定向交易并调用相关智能合约的方式。
但是有一个技巧可以用来将消息调用传递给正确的合约。代理合约不只是对逻辑合约进行常规函数调用;而是使用 Delegatecall。 Delegatecall 类似于常规函数调用,不同之处在于目标地址处的代码是在调用合约的 context 中执行的。如果逻辑合约的代码更改了存储变量,这些更改将反映在代理合约的存储变量中——即在代理合约的状态中。
那么 delegatecall 逻辑在代理合约中的什么位置呢?答案是代理合约的 fallback 函数。当代理合约收到自身无法处理的函数调用时,将调用代理合约的 fallback 函数来处理该函数调用。代理合约在其 fallback 函数中使用自定义逻辑将调用发送到逻辑合约。
将此原则应用于代理和逻辑合约,delegatecall 将调用逻辑合约的代码,但该代码在代理合约的 execution context 中运行。这意味着逻辑合约中的代码有权更改代理合约中的状态——它可以更改存储在代理合约中的状态变量和其他数据。这有效地将应用程序的状态与执行的代码分离。代理合约控制 dApp 的所有状态,也就意味着可以在不丢失 dApp 状态的情况下更改逻辑。
现在 dApp 状态和应用程序逻辑就可以在 EVM 中解耦了,我们可以通过更改逻辑合约并将新地址提供给代理来升级 dApp。但 dApp 的状态不受此升级的影响。
使用代理时,我们需要注意两个常见问题。
一个问题是存储冲突;另一种是代理选择器冲突(proxy selector clashing)。你可以阅读有关存储冲突的链接文章以了解更多信息,但现在我们将重点关注选择器冲突,因为它们是我们将要检查的代理模式的重要原因。
正如我们之前看到的,代理将所有函数调用委托给逻辑合约。但是,代理合约本身也具有函数,这些函数是它们内部的并且是它们运行所必需的。例如,代理合约需要像 upgradeTo(address newAdd) 这样的函数来升级到