如何建立可升级的代理合约以ERC20为例

什么是可升级合约(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

  1. 确认合约是安全的(upgrade safe)
  2. 布署实例合约 Implementation Contract
  3. 布署代理合约管理员 Proxy Admin
  4. 初始化实例合约 Implementation Contract
  5. 布署可升级代理合约 Upgradeability Proxy

注意: 以上步骤是我看完原始码执行跟合约布署状态后理解的顺序,但跟官方文件的顺序不同,大家可以一起研究指正。

升级合约要使用upgradeProxy

  1. 取得proxy admin权限,必须要是管理员才能升级合约
  2. 确认合约是安全的(upgrade safe )
  3. 确认实例合约是不是有被布署过,没有再进行布署
  4. 布署要升级的实例合约
  5. 呼叫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

补充: 储存的问题

  1. 实例合约的地址存在哪?
  2. 实例合约的变数存在哪?

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

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

本文链接地址:https://www.wwsww.cn/jishu/8589.html
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。