在我们进行合约开发时有一个痛点是,升级部署到链上后不能再更改,但如果了解
Solidity
比较深的小伙伴就知道,Solidity
有个delegate
方法,可以实现通过代理合约调用逻辑合约,我们的数据存储在代理合约中,执行的逻辑在逻辑合约中,我们想要升级合约时只需要部署新的逻辑合约即可。具体执行逻辑如下图:
一、简单可升级合约
1 | javascript复制代码// SPDX-License-Identifier: MIT |
代码中包含了3
个合约:
SimpleUpgrade
: 代理合约Logic1
: 旧逻辑合约Logic2
: 新逻辑合约
1. 代理合约SimpleUpgrade
代理合约包含3
个变量:
implementation
: 逻辑合约地址admin
: 合约管理员地址words
: 字符串,通过调用逻辑合约函数来改变
也包含了3
个函数:
- 构造函数: 初始化
admin
和implementation
地址 fallback
函数: 委托函数,会将函数调用委托给逻辑合约执行,需要通过函数选择器calldata
来调用upgrade
函数: 升级函数,只能由admin
调用,改变逻辑合约地址
2. 旧逻辑合约
旧逻辑合约中变量和代理合约保持一致(防止函数执行时插槽错误),通过代理合约调用时改变的状态变量是代理合约中的,有一个函数foo
,将代理合约中的words
值改为old
。
3. 新逻辑合约
和旧逻辑合约逻辑一直,foo
将代理合约中的words
改为new
。
4. 部署测试
- 在
Remix
中首先部署旧逻辑合约(Logic1)
和新逻辑合约(Logic2)
- 再部署
代理合约(SimpleUpgrade)
,构造函数中填入旧逻辑合约(Logic1)
的地址 - 都部署好后可以通过
代理合约
去调用旧逻辑合约
的foo
函数,需要通过低级调用的方式填入函数签名在calldata
中,这里填入c2985578
函数签名可以通过https://abi.hashex.org/
,来生成
- 此时查看
代理合约
中的words
就被改成了old
- 再调用代理合约的
upgrade
函数,填入新逻辑合约
地址,实现逻辑合约的升级 - 最后再次通过低级调用的方式填入函数签名在
calldata
中调用新逻辑合约的foo
函数,就可以看到代理合约中的words
改变为了new
到此,我们就完成了可升级合约的开发和部署,但可升级合约还有可能产生选择器冲突问题。
二、透明代理和通用可升级代理(UUPS)
大家可以看到我们上面填的两个foo
函数的函数签名其实是foo
哈希后取的前 4 个字节,4 个字节这个范围其实很少,两个不同的函数很有可能造成hash
的前 4 个字节一样,这就造成了选择器冲突。
如果选择器冲突出现在同一个合约中,那么合约是无法编译成功的,但是可升级合约会部署两个合约,比如代理合约的升级函数和逻辑合约中其中一个函数有选择器冲突,那么管理人在调用逻辑合约中的函数就可能将代理合约升级成黑洞合约,有严重的安全问题。
解决的方法一般有两种:
- 透明代理
- 通用可升级代理
1. 透明代理
1 | javascript复制代码// SPDX-License-Identifier: MIT |
透明代理是通过限制管理员的权限,管理员只能调用代理合约中的升级函数,不能调用逻辑合约中函数,其他用户只能调用逻辑合约中的函数不能调用代理合约的升级函数来解决选择器冲突问题。
2. 通用可升级代理(UUPS)
1 | javascript复制代码// SPDX-License-Identifier: MIT |
通用可升级代理(UUPS)是通过把升级函数也放在逻辑合约中,代理合约只存储状态变量和调用逻辑合约中的所有函数(升级函数和其他逻辑函数)来解决选择器冲突问题,因为通过代理合约来调用逻辑合约的升级函数时,改变的也是代理合约中存储的逻辑合约的地址,这样我们其实升级也是没有任何问题的。
三、使用Hardhat
+OpenZeppelin
开发生产环境的可升级合约
用上面的办法开发的可升级合约,虽然可以实现可升级功能,但对于一些安全问题没有很好的处理,所以我们一般在实际项目开发中会使用如Hardhat
、OpenZeppelin
等工具来开发可升级合约。
1. 初始化一个Hardhat
项目
1 | bash复制代码npm init |
1 | csharp复制代码$ npx hardhat init |
2. 安装OpenZeppelin
可升级合约的hardhat
插件依赖和合约依赖
1 | bash复制代码npm install --save-dev @openzeppelin/hardhat-upgrades |
3. 合约编写
contracts/Box.sol
:
1 | solidity复制代码// contracts/Box.sol |
合约很简单,就是存储了一个_value
值,并通过store
来修改这个值,并通过retrieve
来读取这个值。
其中有个很关键的initialize
函数,这是合约的初始化函数,在以前我们写构造函数是通过constructor
,但在OpenZeppelin
可升级合约中需要使用initialize
函数。并通过继承Initializable
合约,并在initialize
函数上添加initializer
函数修饰器来确保这个初始化函数只能执行一次。
4. 部署合约
script/deploy.js
1 | javascript复制代码const { ethers, upgrades } = require("hardhat"); |
其中deployProxy
的第二参数为初始化函数需要的参数,通过数组的形式传进去,然后通过运行下面命令来部署,--network
为我自己添加的本地ganache
网络,也可以改成其他网络或者不写,不写会部署到hardhat
的本地测试网络。
1 | bash复制代码npx hardhat run script/deploy.js --network ganache |
5. 可以通过hardhat
提供的console
来测试
1 | bash复制代码npx hardhat console --network ganache |
其中Box.attach
函数需要填入我们上面部署好的Box
合约地址。
6. 编写新的BoxV2
合约
1 | solidity复制代码// SPDX-License-Identifier: MIT |
在BoxV2
合约中我们新增了一个increment
函数,用来增加_value
的值。
7. 编写升级脚本
script/upgrade.js
1 | javascript复制代码const { ethers, upgrades } = require("hardhat"); |
其中upgradeProxy
函数需要填入我们上面部署好的Box
合约地址,并在命令行执行下面命令来升级
1 | bash复制代码npx hardhat run .\scripts\upgrade.js --network ganache |
最后在通过hardhat
的console
来测试发现就多了一个increment
函数了。
1 | bash复制代码npx hardhat console --network ganache |
至此我们就通过
Hardhat
和OpenZeppelin
来实现了一个生产环境可用的可升级合约,大家开发自己的可升级合约时就可以参考这个形式开发即可。
本文转载自: 掘金