可升级的合约和模式
我们知道 Ethereum 上的智能合约是不可升级的,因为代码是不可变的,一旦部署就不能改变。但第一次写出完美的代码是很难的,作为人类,我们都很容易犯错。有时,即使是经过审计的合约,也会发现有一些错误,使其损失数百万。
在这一层,我们将了解一些设计模式,这些模式可以在 Solidity 中用来编写可升级的智能合约。
它是如何工作的?
为了升级我们的合约,我们使用一种叫做代理模式的东西。Proxy 这个词对你来说可能听起来很熟悉,因为它不是一个 web3 的原生词。
从本质上讲,这种模式的工作原理是:一个合约被分成两个合约--代理合约和执行合约。
代理合约负责管理合约的状态,涉及持久性存储,而执行合约负责执行逻辑,不存储任何持久性状态。用户调用代理合约,代理合约进一步对实现合约进行委托调用,这样它就可以实现逻辑。记得我们在以前的一个级别中学习过委托调用。
当实现合约可以被替换时,这种模式就变得有趣了,这意味着执行的逻辑可以被另一个版本的实现合约所替换,而不影响存储在代理中的合约的状态。
主要有三种方式,我们可以替换/升级实现合约。
- 钻石实现
- 透明的实现
- UUPS 实现
然而,我们将只关注透明和 UUPS,因为它们是最常用的。
要升级实施合约,你必须使用一些方法,比如 upgradeTo(address)
,这基本上会把实施合约的地址从旧的变成新的。
但重要的部分在于我们应该把 upgradeTo(address)
函数放在哪里,我们有两个选择,要么把它放在代理合约中,这基本上是透明代理模式的工作方式,要么把它放在执行合约中,这就是 UUPS 合约的工作方式。
关于这个代理模式,另一个需要注意的是,实现合约的构造器永远不会被执行。
当部署一个新的智能合约时,构造器内的代码不是合约运行时字节码的一部分,因为它只在部署阶段需要,并且只运行一次。现在,因为当实施合约被部署时,它最初没有连接到代理合约,作为一个原因,任何在构造器中发生的状态变化现在在代理合约中不存在,而代理合约是用来维护整体状态的。
作为一个原因,代理合约不知道构造函数的存在。因此,我们不使用构造函数,而是使用一个叫做初始化函数的东西,一旦实现合约与之连接,代理合约就会调用这个函数。这个函数所做的正是构造函数应该做的事情,但是现在它被包含在运行时字节码中,因为它的行为就像一个普通的函数,并且可以被代理合约调用。
使用 OpenZeppelin 合约,你可以使用他们的 Initialize.sol 合约,它可以确保你的初始化函数只被执行一次,就像构造函数一样
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyContract is Initializable {
function initialize(
address arg1,
uint256 arg2,
bytes memory arg3
) public payable initializer {
// "constructor" code...
}
}
上面给出的代码来自 Openzeppelin 的文档,并提供了一个示例,说明初始化器修饰符
如何确保初始化函数
只能被调用一次。这个修饰符来自 Initializable Contract
我们现在将详细研究代理模式 🚀 👀
透明代理模式
透明代理模式是一种分离代理和实现合约之间职责的简单方法。在这种情况下,upgradeTo 函数是代理合约的一部分,并且可以通过在代理上调用 upgradeTo 来升级实现,从而改变未来函数调用的委托位置。
不过有一些警告。可能存在代理合约和实施合约具有相同名称和参数的函数的情况。想象一下,如果 Proxy Contract 有一个 owner() 函数,Implementation Contract 也有。在透明代理合约中,这个问题由代理合约处理,它决定来自用户的调用是在代理合约本身内执行,还是在基于 msg.sender 全局变量的实施合约中执行
因此,如果 msg.sender 是代理的管理员,那么代理将不会委托调用,并且如果它理解它会尝试执行调用。如果不是管理员地址,即使匹配代理的功能之一,代理也会将调用委托给实施合约。