什么是可升级合约(Upgradable Contract)?
顾名思义,就是可以升级的合约。(被打)
一般来说,区块链最令人耳熟能详的就是不可窜改性,任何程序代码只要上链了就不能够更改了,这赋予了区块链最强大的功能,然后反面过来思考就是,万一你个合约写坏了,你也没有办法去更改,这不符合软体产业快速迭代的特性了,可升级合约就是为了解决此问题,以下我们会介绍这跟一般合约有什么不同,接着会教学建立的步骤。
合约架构
可升级合约就是利用代理合约去实现升级的效果,如下图所示,我们把一张合约分拆成Proxy Contract跟Logic Contract,将资料存在代理合约和程序逻辑储存在逻辑合约中,所以升级的时候,旧有的资料并不会消失,而是会继续保留在合约中,而抽象的逻辑就可以随着升级的合约更新。
以上是最简单的代理合约模型,你点进去来源网址会发现,实际上的代理合约模式是更复杂的。但在合约的架构上可以分为三种,当你第一次布署代理合约的时候就会发现,共有三个合约被布署,分别是代理合约管理员Proxy Admin、可升级代理合约Upgradeability Proxy、实例合约Implementation Contract,以下分别介绍:
- 实例合约Implementation Contract:可被升级逻辑合约,可以藉由每次布署不同的合约达到改变逻辑的效果,要注意的是变数等储存资讯是不能被改动的,会导致合约崩溃。
- 代理合约管理员Proxy Admin:储存代理合约的拥有者,只有拥有者才能升级合约,并且在升级的时候呼叫Upgradeability Proxy更新Implementation Contract的地址。
- 可升级代理合约Upgradeability Proxy:代理合约本人,地址永远不变,所有使用者直接对该合约进行操作,会储存Implementation Contract的地址。
代理合约跟一般合约的不同点
solidity中的constructor并不是runtime bytecode的一部分,只会在布署的过程中运行一次,所以代理合约无法使用实例合约的constructor,因为已经在布署时运行过了,因此我们把要把实例合约的的程序代码移到initializefunction中,如此就不会被solidity限制。
// contracts/MyContract.sol // SPDX-License-Identifier: MIT pragma solidity ^0.6.0;import "@openzeppelin/upgrades/contracts/Initializable.sol";contract MyContract is Initializable { uint256 public x; function initialize(uint256 _x) public initializer { x = _x; } }
还有一个不同的地方,Solidity会自动启动其他父层合约的constructor,但在initializer的状况中,你需要手动处理。
// contracts/MyContract.sol // SPDX-License-Identifier: MIT pragma solidity ^0.6.0;import "@openzeppelin/upgrades/contracts/Initializable.sol";contract BaseContract is Initializable { uint256 public y; function initialize() public initializer { y = 42; } }contract MyContract is BaseContract { uint256 public x; function initialize(uint256 _x) public initializer { BaseContract.initialize(); // Do not forget this call! x = _x; } }
初始值跟constructor一样只有deploy时有作用,因此要将值放在initialize中
//正确 contract MyContract is Initializable { uint256 public hasInitialValue; function initialize() public initializer { hasInitialValue = 42; // set initial value in initializer } }//错误 contract MyContract { uint256 public hasInitialValue = 42; function initialize() public initializer { } }
布署过程
布署代理合约的过程很繁琐,所以我们采用openzeppelin-upgrades的外挂插件,这个外挂会把复杂的布署一次处理完毕,以下来介绍这个外挂做了什么事情。
布署合约时要使用 deployProxy
- 确认合约是安全的(upgrade safe)
- 布署实例合约 Implementation Contract
- 布署代理合约管理员 Proxy Admin
- 初始化实例合约 Implementation Contract
- 布署可升级代理合约 Upgradeability Proxy
注意: 以上步骤是我看完原始码执行跟合约布署状态后理解的顺序,但跟官方文件的顺序不同,大家可以一起研究指正。
升级合约要使用upgradeProxy
- 取得proxy admin权限,必须要是管理员才能升级合约
- 确认合约是安全的(upgrade safe )
- 确认实例合约是不是有被布署过,没有再进行布署
- 布署要升级的实例合约
- 呼叫Proxy Admin合约,更新代理合约上的实例合约地址
补充:如果Implementation Contract的程序代码没有改变,但又布署一次proxy的话,则impl. contact不会再被deploy,仅会布署proxy contract。
https://github.com/OpenZeppelin/openzeppelin-upgrades
布署ERC20 代理合约
接下来我们就开始运行我们的程序代码吧,环境使用hardhat。
安装hardhat,选择建立空的config
$ npm install --save-dev hardhat$ npx hardhat Welcome to Hardhat v2.0.2 ✔ What do you want to do? · Create an empty hardhat.config.js Config file created
用hardhat建链(我个人是习惯用ganache )
$ npx hardhat node
设定hardhat.config.js,根据你的网络设定调整,可参考文件
/** * @type import('hardhat/config').HardhatUserConfig */ require('@nomiclabs/hardhat-ethers'); require('@openzeppelin/hardhat-upgrades');module.exports = { defaultNetwork: "ganache", networks: { ganache: { url: "http://172.17.144.1:7545", // accounts: [privateKey1, privateKey2, ...] } }, solidity: { version: "0.6.12", }, };
建立合约
// SPDX-License-Identifier: MIT pragma solidity >=0.6.0 <0.7.5; import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/ ERC20Upgradeable.sol";contract TestToken is Initializable, ERC20Upgradeable { function initialize(string memory name_, string memory symbol_, uint256 initialSupply) public virtual initializer { __ERC20_init(name_, symbol_); _mint(msg.sender, initialSupply); } }
建立布署合约程序代码
const { ethers, upgrades } = require("hardhat");async function main() { const TestToken = await ethers.getContractFactory("TestToken"); const testToken = await upgrades.deployProxy(TestToken, ['TestToken', 'TST', 100000000000]); await testToken.deployed(); console .log("testToken deployed to:", testToken.address); } main();
布署合约
npx hardhat run ./scripts/erc20-deploy-proxy.js
取得合约资讯,记得把地址改为生成的合约地址
const { BigNumber } = require("ethers"); const { ethers, upgrades } = require("hardhat"); async function main() { const address = "0x8675Cfe9ef7815f43E08e87cda8438F5D7AAF5Fe"; const TestToken = await ethers.getContractFactory("TestToken" ); const testToken = await TestToken.attach(address); var totalSupply = await testToken.totalSupply(); console.log("testToken totalSupply:", totalSupply.toString()); const balances = ["0xF89fA5bC76F5C945FAb248bb50fDA846774a9BF9", "0xEd5aa8E471D012e18BeF2A35ADE4501d7Afe51c6 ", "0x2B2443067B14B989B488012cBb147b68EaC02891"]; balances.forEach((account, i) => { var qqq = testToken.balanceOf(account).then(value => { console.log("account", i, "balance: ", value.toString()) return value }); }); }main() .then() .catch(error => { console.error(error); process.exit(1); });
其他操作可以参考我的github : https://github.com/cfengliu/upgradable-contract
补充: 储存的问题
- 实例合约的地址存在哪?
- 实例合约的变数存在哪?
2.会存在代理合约上:
因为使用delegatecall的关系,代理合约storage slot会储存变数的值,实例合约的变数会指到proxy合约的变数。
本文链接地址:https://www.wwsww.cn/jishu/8589.html
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。