如何建立可升级的代理合约以ERC20为例
Posted 软件工程小施同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何建立可升级的代理合约以ERC20为例相关的知识,希望对你有一定的参考价值。
什么是可升级合约(Upgradable Contract)?
顾名思义,就是可以升级的合约。(被打)
一般来说,区块链最令人耳熟能详的就是不可窜改性,任何程序代码只要上链了就不能够更改了,这赋予了区块链最强大的功能,然后反面过来思考就是,万一你个合约写坏了,你也没有办法去更改,这不符合软体产业快速迭代的特性了,可升级合约就是为了解决此问题,以下我们会介绍这跟一般合约有什么不同,接着会教学建立的步骤。
合约架构
可升级合约就是利用代理合约去实现升级的效果,如下图所示,我们把一张合约分拆成Proxy Contract跟Logic Contract,将资料存在代理合约和程序逻辑储存在逻辑合约中,所以升级的时候,旧有的资料并不会消失,而是会继续保留在合约中,而抽象的逻辑就可以随着升级的合约更新。
来源: Proxy Patterns - OpenZeppelin blog
以上是最简单的代理合约模型,你点进去来源网址会发现,实际上的代理合约模式是更复杂的。但在合约的架构上可以分为三种,当你第一次布署代理合约的时候就会发现,共有三个合约被布署,分别是代理合约管理员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。
布署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 : GitHub - cfengliu/upgradable-contract
补充: 储存的问题
- 实例合约的地址存在哪?
- 实例合约的变数存在哪?
2.会存在代理合约上:
因为使用delegatecall的关系,代理合约storage slot会储存变数的值,实例合约的变数会指到proxy合约的变数。
以上是关于如何建立可升级的代理合约以ERC20为例的主要内容,如果未能解决你的问题,请参考以下文章