首页 > 链百科 > 如何建立可升级的代理合约以ERC20为例

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

2025-09-13 10:01:59

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

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

ERC20

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

合约架构

可升级合约就是利用代理合约去实现升级的效果,如下图所示,我们把一张合约分拆成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合约的变数。

声明:文章不代表链懂观点及立场,不构成本平台任何投资建议。投资决策需建立在独立思考之上,本文内容仅供参考,风险自担!转载请注明出处!侵权必究!
相关阅读相关阅读
热门资讯热门资讯
风险
提示

链懂数据及信息均来源公开资料,不构成任何推荐或投资建议。炒币属投资行为,市场有风险,投资需谨慎。

闽ICP备2023001858号-1 站点地图
Copyright ©2025 链懂.All Rights Reserved