Web3系列教程之高级篇---10:构建可随时间升级的智能合约
我们知道,以太坊上的智能合约是不能升级的,因为代码是不可变的,一旦部署就不能更改。但第一次写出完美的代码是很难的,作为人类,我们都很容易犯错。有时,即使是经过审计的合同,也会发现有错误,导致其损失数百万。
在本文中,我们将学习一些可以在Solidity中使用的设计模式,以编写可升级的智能合约。
它是如何工作的?
为了升级我们的合同,我们使用了一种叫做Proxy Pattern
的东西。Proxy
这个词对你来说可能听起来很熟悉,因为它不是一个web3的原生词。
从本质上讲,这种模式的工作原理是,一个合同被分成两个合同–Proxy Contract
和Implementation Contract
。
Proxy Contract
负责管理合同的状态,涉及持久性存储,而Implementation Contract
负责执行逻辑,不存储任何持久性状态。用户调用Proxy Contract
,Proxy Contract
进一步对实现合同进行delegatecall
,这样它就可以实现逻辑。记得我们在以前的文章中学习过delegatecall
👀。
当Implementation Contract
可以被替换时,这种模式就变得有趣了,这意味着执行的逻辑可以被另一个版本的Implementation Contract
所替换,而不影响存储在代理中的合同的状态。
主要有三种方式,我们可以替换/升级Implementation Contract
:
- Diamond Implementation
- Transparent Implementation
- UUPS Implementation
然而,我们将只关注Transparent和UUPS,因为它们是最常用的。
要升级Implementation Contract
,你必须使用一些方法,比如upgradeTo(address)
,这基本上会把Implementation Contract
的地址从旧的变成新的。
但重要的部分在于我们应该把upgradeTo(address)
函数放在哪里,我们有两个选择,要么把它放在Proxy Contract
中,这基本上是Transparent Proxy Pattern
的工作方式,要么把它放在Implementation Contract
中,这就是UUPS合同的工作方式。
关于这个Proxy Pattern
,另一个需要注意的是,Implementation Contract
的构造函数永远不会被执行。
当部署一个新的智能合约时,构造器内的代码不是合约运行时字节码的一部分,因为它只在部署阶段需要,并且只运行一次。现在,因为当Implementation Contract
被部署时,它最初没有连接到Proxy Contract
,因此,任何在构造器中发生的状态变化现在在Proxy Contract
中不存在,而Proxy Contract
是用来维护整体状态的。
因为Proxy Contracts
不知道构造函数的存在。因此,我们不使用构造函数,而是使用一个叫做initializer
函数的东西,一旦Implementation Contract
与之相连,它就会被Proxy Contract
调用。这个函数所做的正是构造函数应该做的事情,但是现在它被包含在运行时字节码中,因为它的行为就像一个普通的函数,并且可以被Proxy Contract
调用。
使用OpenZeppelin合约,你可以使用他们的Initialize.sol
合约,确保你的initialize
函数只被执行一次,就像一个构造函数一样
|
|
上面给出的代码来自Openzeppelin的文档,它提供了一个例子,说明initializer
修改器如何确保初始化函数只能被调用一次。这个修改器来自Initializable Contract
我们现在将详细研究代理模式🚀 👀
透明代理模式
透明代理模式是一种简单的方法来分离Proxy
合同和Implementation
合同之间的责任。在这种情况下,upgradeTo
函数是Proxy
合同的一部分,而Implementation
可以通过在Proxy上调用upgradeTo
来升级,从而改变未来函数调用的委托位置。
不过也有一些注意事项。可能有这样一种情况:Proxy Contract
和Implementation Contract
有一个名称和参数相同的函数。想象一下,如果Proxy Contract
有一个owner()
函数,Implementation Contract
也有。在透明代理合约中,这个问题由Proxy Contract
来处理,Proxy Contract
根据msg.sender
全局变量来决定用户的调用是在Proxy Contract
本身还是在Implementation Contract
中执行。
所以如果msg.sender
是代理的管理员,那么代理将不会委托调用,如果它理解的话,将尝试执行调用。如果它不是管理员地址,代理将把调用委托给Implementation Contract
,即使它与代理的某个函数相匹配。
透明代理模式的问题
我们知道,owner
的地址必须存储在存储器中,而使用存储器是与智能合约互动的最低效和最昂贵的步骤之一,每次用户调用代理时,代理会检查用户是否是管理员,这给大多数发生的交易增加了不必要的气体成本。
UUPS代理模式
UUPS代理模式是另一种在Proxy
合同和Implementation
合同之间分离责任的方式。在这种情况下,upgradeTo
函数也是Implementation
契约的一部分,并且通过代理被所有者使用delegatecall
。
在UUPS中,不管是管理员还是用户,所有的调用都被发送到Implementation Contract
中。这样做的好处是,每次调用时,我们不必访问存储空间来检查开始调用的用户是否是管理员,这提高了效率和成本。另外,因为是Implementation Contract
,你可以根据你的需要定制功能,在每一个新的Implementation
中加入诸如Timelock
、Access Control
等,这在Transparent Proxy Pattern
中是做不到的。
UUPS代理模式的问题
现在的问题是,因为upgradeTo
函数存在于Implementation contract
的一侧,开发者必须担心这个函数的实现,这有时可能很复杂,而且因为增加了更多的代码,增加了攻击的可能性。这个函数也需要在所有被升级的Implementation contract
的版本中出现,这就引入了一个风险,如果开发者忘记添加这个函数,那么合同就不能再被升级了。
构建
让我们开发一个例子,你可以体验如何建立一个可升级的合同。在这个例子中,我们将使用UUPS可升级模式,当然你也可以用透明代理模式来开发一个。
- 要设置一个Hardhat项目,请打开终端并执行这些命令
|
|
- 如果你使用的是Windows系统,请做这个额外的步骤,同时安装这些库 :)
|
|
- 在你安装Hardhat的同一目录下运行。
|
|
- 选择
Create a basic sample project
- 对已指定的
Hardhat Project root
按回车键 - 如果你想添加一个
.gitignore
,请按回车键。 - 按回车键
Do you want to install this sample project's dependencies with npm (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)?
现在你有一个准备好的hardhat项目了!
- 我们将使用openzeppelin的库,它支持可升级的合同。要安装这些库,在同一个文件夹中执行以下命令。
|
|
用以下代码替换你的hardhat.config.js
中的代码,以便能够使用这些库。
|
|
首先,在contracts
目录下创建一个新文件,名为LW3NFT.sol
,并在其中添加以下几行代码
|
|
让我们试着更详细地了解这份合同中所发生的事情。
如果你看一下LW3NFT
导入的所有合约,你就会明白为什么它们很重要。首先是Openzeppelin的Initializable
契约,它为我们提供了initializer
修改器,确保initializer
函数只被调用一次。initializer
函数是需要的,因为我们不能在Implementation Contract
中拥有一个构造器,在这种情况下,Implementation Contract
就是LW3NFT
的契约
它导入了ERC721Upgradeable
和OwnableUpgradeable
,因为原始的ERC721
和Ownable
合约有一个构造函数,不能用于代理合约。
最后,我们有UUPSUpgradeable Contract
,它为我们提供了upgradeTo(address)
函数,在UUPS
代理模式的情况下,它必须被放在Implementation Contract
上。
在合同的声明之后,我们有一个带有initialize
修饰的initialize
函数,我们从Initializable
合同中得到。initialize
修饰符确保initialize
函数只能被调用一次。还请注意,我们初始化ERC721
和Ownable
合约的新方式。这是初始化可升级合约的标准方式,你可以在这里看一下这个函数。之后,我们就用通常的mint函数来造币。
|
|
另一个有趣的功能是_authorizeUpgrade
,我们在正常的ERC721
合约中没有看到这个功能,当开发者从Openzeppelin导入UUPSUpgradeable Contract
时,需要实现这个功能,它可以在这里找到。现在,为什么这个函数必须被覆盖,这很有趣,因为它让我们有能力在谁能真正升级给定的合同上添加授权,它可以根据要求改变,但在我们的案例中,我们只是添加了一个onlyOwner
修改器。
|
|
现在让我们在contracts
目录下创建另一个新文件,名为LW3NFT2.sol
,这将是LW3NFT.sol
的升级版。
|
|
这个智能合约要简单得多,因为它只是继承了LW3NFT
合约,然后添加了一个名为test
的新函数,它只是返回一个upgraded
的字符串。
很容易吧?🤯
Wow 🙌 ,好了,我们已经写完了Implementation Contract
,现在我们还需要写 Proxy Contract
吗?
好消息是,不,我们不需要写Proxy Contract
,因为当我们使用Openzeppelin
的库来部署Implementation Contract
时,Openzeppelin会自动部署和连接一个Proxy Contract
。
因此,让我们尝试这样做,在你的test
目录中创建一个新的文件,命名为proxy-test.js
,让我们用代码尝试一下
|
|
让我们看看这里发生了什么,我们首先使用getContractFactory
函数获得LW3NFT
和LW3NFT
实例,这是我们到现在为止一直在教的所有文章都通用的。在这之后,最重要的一行出现了。
|
|
这个函数来自于你安装的@openzeppelin/hardhat-upgrades
库,它基本上是使用upgrades类来调用deployProxy
函数,并指定种类为uups
。当该函数被调用时,它将部署Proxy Contract
、LW3NFT Contract
并将它们连接起来。关于这个的更多信息可以在这里找到。
请注意,initialize
函数可以用任何其他名字,只是deployProxy
默认调用initialize
函数,但你可以通过改变默认值来修改它 😇
部署后,我们通过调用代币ID 1的ownerOf
函数来测试合约是否真的被部署,并检查NFT是否真的被铸造。
现在,下一部分来了,我们要部署LW3NFT2
,这是LW3NFT
的升级版合同。
为此,我们再次执行@openzeppelin/hardhat-upgrades
库中的upgradeProxy
方法,该方法将LW3NFT
升级并替换为LW3NFT2
,而不改变系统的状态。
|
|
为了测试它是否真的被替换,我们调用了test()
函数,并确保它返回 "upgraded"
,尽管该函数在最初的LW3NFT
合同中并不存在。
你今天学会了如何升级一个智能合约。
LFG 🚀
阅读
参考
如果你觉得这篇文章对你有所帮助,欢迎赞赏~
赞赏