什么是可升级合约(Upgradable Contract)

顾名思义,就是可以升级的合约。(被打)

一般来说,区块链最令人耳熟能详的就是不可窜改性,任何程序代码只要上链了就不能够更改了,这赋予了区块链最强大的功能,然后反面过来思考就是,万一你个合约写坏了,你也没有办法去更改,这不符合软体产业快速迭代的特性了,可升级合约就是为了解决此问题,以下我们会介绍这跟一般合约有什么不同,接着会教学建立的步骤。

合约架构

可升级合约就是利用代理合约去实现升级的效果,如下图所示,我们把一张合约分拆成Proxy Contract跟Logic Contract,将资料存在代理合约和程序逻辑储存在逻辑合约中,所以升级的时候,旧有的资料并不会消失,而是会继续保留在合约中,而抽象的逻辑就可以随着升级的合约更新。

来源:https://blog.openzeppelin.com/proxy-patterns/

以上是最简单的代理合约模型,你点进去来源网址会发现,实际上的代理合约模式是更复杂的。但在合约的架构上可以分为三种,当你第一次布署代理合约的时候就会发现,共有三个合约被布署,分别是代理合约管理员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

补充: 储存的问题

实例合约的地址存在哪 实例合约的变数存在哪

Ans:

1.存在代理合约:https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/6ffc421f0db0c8ab5dad19b978e50f59aa6ef1b9/packages/core/contracts/proxy/UpgradeabilityProxy.sol#L69

2.会存在代理合约上:

因为使用delegatecall的关系,代理合约storage slot会储存变数的值,实例合约的变数会指到proxy合约的变数。