之前讨论了如何使用OpenZeppelin 提供的代理 升级模式来建立可升级的智能合约。本文我想透过探索可升级的ERC20 合约来更深入地研究这个主题。
代理升级模式
尽管部署在区块链上的程序代码是不可变的,但这种模式允许我们透过使用两个合约来修改它:代理程序和实现合约。

主要想法是用户将始终与代理合约交互,代理合约将呼叫转发给实现合约。要更改程序代码,我们只需要部署新版本的实作合约,并将代理设定为指向这个新的实作。
ERC-20 代币
Vitalik Buterin 和Fabian Vogelsteller 建立了一个开发可替代代币的框架,可替代代币是一类数位资产或代币,可以在一对一的基础上与相同的对应物互换。该框架被认可为ERC-20 标准。
OpenZeppelin 的可升级合约
当使用代理升级模式建立可升级的智能合约时,不可能使用建构函式。为了解决此限制,必须用常规函数(通常称为初始值设定项)来取代建构函数。这些初始化程序通常称为“initialize”,用作建构函数逻辑的储存库。
然而,我们应该确保初始化函数像建构函数一样只被呼叫一次,因此OpenZeppelin 提供了一个带有初始化修饰符的可初始化基本合约来处理这个问题:
// contracts/MyContract.sol // SPDX-License-Identifier: MIT pragma solidity ^ 0.6 .0 ; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol" ; contract MyContract is Initializable { uint256 public x; function initialize ( uint256 _x ) public initializer { x = _x; } }
此外,由于建构函数会自动呼叫所有合约祖先的建构函数,因此也应该在我们的初始化函数中实现,而OpenZeppelin 允许我们透过使用onlyInitializing修饰符来实现此目的:
// contracts/MyContract.sol // SPDX-License-Identifier: MIT pragma solidity ^ 0.6 .0 ; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol" ; contract BaseContract is Initializable { uint256 public y; function initialize () public onlyInitializing { y = 42 ; } } contract MyContract is BaseContract { uint256 public x; function initialize ( uint256 _x ) public initializer { BaseContract.initialize(); // Do not forget this call! x = _x; } }
考虑到上述几点,利用OpenZeppelin 的标准ERC-20 合约来创建可升级代币是不可行的。事实上,因为它们有一个建构函数,所以应该用初始化器来取代它:
// @openzeppelin/contracts/token/ERC20/ERC20.sol pragma solidity ^ 0.8 .0 ; ... contract ERC20 is Context , IERC20 { ... string private _name; string private _symbol; constructor ( string memory name_, string memory symbol_ ) { _name = name_; _symbol = symbol_; } ... }
然而,OpenZeppelin 透过合约的分支提供了一个解决方案:openzeppelin/contracts-upgradeable。在此修改版本中,建构函式已被初始化程序取代,从而允许以更大的灵活性创建可升级的令牌。
例如ERC20合约已被ERC20Upgradeable取代:
// @openzeppelin/contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol pragma solidity ^ 0.8 .0 ; ... contract ERC20Upgradeable is Initializable , ContextUpgradeable , IERC20Upgradeable { ... string private _name; string private _symbol; function __ERC20_init ( string memory name_, string memory symbol_ ) internal onlyInitializing { __ERC20_init_unchained (name_, symbol_); } function __ERC20_init_unchained ( string memory name_, string memory symbol_ ) internal onlyInitializing { _name = name_; _symbol = symbol_; } ... }
动手演示
为了创建新的ERC20 可升级代币,我使用了OpenZeppelin 向导来简化合约的创建:

我选择将令牌称为UpgradeableToken,并设定了以下功能:

合约第一版
这是向导为我产生的程序代码:
// SPDX-License-Identifier: MIT pragma solidity ^ 0.8 .23 ; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol" ; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable .sol" ; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol" ; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol" ; import "@openzeppelin/contracts-upgradeable/proxy /utils/Initializable.sol" ; contract UpgradeableToken1 is Initializable , ERC20Upgradeable , ERC20BurnableUpgradeable , ERC20PausableUpgradeable , OwnableUpgradeable { /// @custom:oz-upgrades-unsafe-allow constructor constructor ( ) { _disableInitializers (); } function initialize ( address initialOwner ) initializer public { __ERC20_init ( "UpgradeableToken" , "UTK" ); __ERC20Burnable_init (); __ERC20Pausable_init (); __Ownable_init (initialOwner); _mint (msg. sender , 10000 * 10 ** decimals ()); } function pause ( ) public onlyOwner { _pause (); } function unpause ( ) public onlyOwner { _unpause (); } function mint ( address to, uint256 amount ) public onlyOwner { _mint (to, amount); } // The following functions are overrides required by Solidity. function _update ( address from , address to, uint256 value ) internal override ( ERC20Upgradeable, ERC20PausableUpgradeable ) { super . _update ( from , to, value); } }
该程序代码代表我们合约的第一个版本。
我在此存储库中创建了一个Hardhat 项目,其中包含整个程序代码。如果您是Hardhat 新手,可以参考本指南,我在其中解释了如何从头开始建立新项目。
为了测试前一份合约的主要功能,我创建了以下脚本:
import { expect } from "chai" ; import { ethers, upgrades } from "hardhat" ; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" ; import { UpgradeableToken1 , UpgradeableToken1 __factory } from "../typechain- types" ; describe ( "Contract version 1" , () => { let UpgradeableToken1 : UpgradeableToken1 __factory; let token : UpgradeableToken1 ; let owner : HardhatEthersSigner ; let addr1 : HardhatEthersSigner ; let addr2 : HardhatEthersSigner ; const DECIMALS : bigint = 10n ** 18n ; const INITIAL_SUPPLY : bigint = 10_000n ; beforeEach ( async () => { UpgradeableToken1 = await ethers. getContractFactory ( "UpgradeableToken1" ); [owner, addr1, addr2] = await ethers. getSigners (); token = await upgrades. deployProxy ( UpgradeableToken1 , [owner. address ], { initializer : 'initialize' , kind : 'transparent' }); await token. waitForDeployment (); }); describe ( "Deployment" , () => { it ( "Should set the right name" , async () => { expect ( await token. name ()). to . equal ( "UpgradeableToken" ); }); it ( "Should set the right symbol" , async () => { expect ( await token. symbol ()). to .equal ( "UTK"); }); it ( "Should set the right owner" , async () => { expect ( await token. owner ()). to . equal (owner. address ); }); it ( "Should assign the initial supply of tokens to the owner" , async () => { const ownerBalance = await token. balanceOf (owner. address ); expect (ownerBalance). to . equal ( INITIAL_SUPPLY * DECIMALS ); expect ( await token. totalSupply ()) . to . equal (ownerBalance); }); }); describe ( "Transactions" , () => { it ( "Should transfer tokens between accounts" , async () => { // Transfer 50 tokens from owner to addr1 await token. transfer (addr1. address , 50 ); const addr1Balance = await token. balanceOf (addr1. address ); expect (addr1Balance). to . equal ( 50 ); // Transfer 50 tokens from addr1 to addr2 // We use .connect(signer) to send a transaction from another account await token. connect (addr1). transfer (addr2. address , 50 ); const addr2Balance = await token. balanceOf (addr2. address ); expect (addr2Balance). to . equal ( 50 ); }); it ( "Should fail if sender doesn't have enough tokens" , async () => { const initialOwnerBalance = await token. balanceOf (owner. address ); // Try to send 1 token from addr1 (0 tokens) to owner (1000000 tokens). expect ( token. connect (addr1).transfer (owner. address , 1 ) ). to . be . revertedWithCustomError ; // Owner balance shouldn't have changed. expect ( await token. balanceOf (owner. address )). to . equal ( initialOwnerBalance ); }); it ( "Should update balances after transfers" , async () => { const initialOwnerBalance : bigint = await token. balanceOf (owner. address ); // Transfer 100 tokens from owner to addr1. await token. transfer (addr1. address , 100 ); // Transfer another 50 tokens from owner to addr2. await token. transfer (addr2. address , 50 ); // Check balances. const finalOwnerBalance = await token. balanceOf (owner. address ); expect (finalOwnerBalance). to . equal (initialOwnerBalance - 150n ); const addr1Balance = await token. balanceOf (addr1. address ); expect (addr1Balance). to . equal ( 100 ); const addr2Balance = await token. balanceOf (addr2. address ); expect (addr2Balance). to . equal ( 50 ); }); }); describe ( "Minting" , () => { it ( "It should mint tokens to the owner's address" , async () => { await token. mint (owner. address , 10n * DECIMALS ); const ownerBalance : bigint = await token. balanceOf (owner. address ); expect (ownerBalance). to .equal (( INITIAL_SUPPLY + 10n ) * DECIMALS ); }); }); describe ( "Burning" , () => { it ( "Should burn tokens from the owner's address" , async () => { await token. burn ( 10n * DECIMALS ); const ownerBalance : bigint = await token. balanceOf (owner. address ); expect (ownerBalance). to . equal (( INITIAL_SUPPLY - 10n ) * DECIMALS ); }); }); describe ( "Pauseable features " , () => { it ( "Should pause the contract" , async () => { await token. pause (); expect ( await token. paused ()). to . be . true expect ( token. transfer ( addr1. address , 50 )). to . be . revertedWithCustomError ; }); it ( "Should unpause the contract" , async () => { await token. pause (); await token. unpause (); expect ( await token . paused ()). to . be . false expect ( await token. transfer (addr1. address , 50 )). not . throw ; }); }); });
合约第二版
现在让我们想像一下,我们想要添加一个包含黑名单的新合约,该黑名单将冻结特定帐户,防止其转移、接收或销毁代币。
为了完成此任务,我创建了UpgradeableToken 的新版本,使其继承实现黑名单机制的BlackList 合约:
// SPDX-License-Identifier: MIT pragma solidity ^ 0.8 .23 ; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol" ; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable .sol" ; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol" ; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol" ; import "@openzeppelin/contracts-upgradeable/proxy /utils/Initializable.sol" ; contract BlackList is OwnableUpgradeable { mapping (address => bool ) internal blackList; function isBlackListed ( address maker ) public view returns ( bool ) { return blackList[maker]; } function addBlackList ( address evilUser ) public onlyOwner { blackList[evilUser] = true ; emit AddedBlackList ( evilUser ) ; } function removeBlackList ( address clearedUser ) public onlyOwner { blackList[clearedUser] = false ; emit RemovedBlackList ( clearedUser ) ; } event AddedBlackList ( address user ) ; event RemovedBlackList ( address user ) ; } contract UpgradeableToken2 is Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable, ERC20PausableUpgradeable, OwnableUpgradeable, BlackList { /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function pause () public onlyOwner { _pause (); } function unpause () public onlyOwner { _unpause(); } function mint ( address to, uint256 amount )public onlyOwner { _mint(to, amount); } // The following functions are overrides required by Solidity. function _update(address from , address to, uint256 value ) internal override ( ERC20Upgradeable, ERC20PausableUpgradeable ) { require(!isBlackListed( from ), "The sender address is blacklisted" ); require(!isBlackListed(to), "The recipient address is blacklisted" ); super._update( from , to, value ); } }
为了防止列入黑名单的地址传输、接收、刻录或获取一些新铸造的代币,已在_update函数中添加了两个要求。事实上,每当我们呼叫transfer、burn和mint函数时,这个函数就会被呼叫。
为了测试合约的第二个版本及其新功能,使用了以下脚本:
import { expect } from "chai" ; import { ethers, upgrades } from "hardhat" ; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" ; import { UpgradeableToken2, UpgradeableToken2__factory } from "../typechain-types " ; describe( "Contract version 2" , () => { let UpgradeableToken2: UpgradeableToken2__factory; let newToken: UpgradeableToken2; let owner: HardhatEthersSigner; let addr1: HardhatEthersSigner; let addr2: HardhatEthersSigner; const DECIMALS: bigint = 10 n ** 18 n; const INITIAL_SUPPLY: bigint = 10 _000n; beforeEach( async () => { const UpgradeableToken1 = await ethers.getContractFactory( "UpgradeableToken1" ); UpgradeableToken2 = await ethers.getContractFactory( 'UpgradeableToken2' ); [owner, addr1, addr2] = await ethers.getSigners(); const oldToken = await upgrades.deployProxy(UpgradeableToken1, [owner.address], { initializer: 'initialize' , kind: 'transparent' }); await oldToken.waitForDeployment(); newToken = await upgrades .upgradeProxy(oldToken, UpgradeableToken2, { kind: 'transparent' }); }); describe( "Deployment" , () => { it( "Should set the right name" , async () => { expect( await newToken .name()).to.equal( "UpgradeableToken" ); }); it( "Should set the right symbol" , async () => { expect( await newToken.symbol()).to.equal( "UTK " ); }); it( "Should set the right owner" , async () => { expect( await newToken.owner()).to.equal(owner.address); }); it( "Should assign the initial supply of tokens to the owner" ,async () => { const ownerBalance = await newToken.balanceOf(owner.address); expect(ownerBalance).to.equal(INITIAL_SUPPLY * DECIMALS); expect( await newToken.totalSupply()).to.equal(ownerBalance); }); }); describe( "Transactions" , () => { it( "Should transfer tokens between accounts" , async () => { // Transfer 50 tokens from owner to addr1 await newToken.transfer(addr1.address, 50 ); const addr1Balance = await newToken.balanceOf(addr1.address); expect( addr1Balance).to.equal( 50 ); // Transfer 50 tokens from addr1 to addr2 // We use .connect(signer) to send a transaction from another account await newToken.connect(addr1).transfer(addr2.address, 50 ); const addr2Balance = await newToken.balanceOf(addr2.address); expect(addr2Balance).to.equal( 50 ); }); it( "Should fail if sender doesn't have enough tokens" , async () => { const initialOwnerBalance = await newToken.balanceOf(owner.address); // Try to send 1 token from addr1 (0 tokens) to owner (1000000 tokens). expect( newToken.connect(addr1).transfer(owner.address, 1 ) ).to.be.revertedWithCustomError; // Owner balance shouldn't have changed. expect( await newToken.balanceOf(owner.address)).to.equal( initialOwnerBalance ); }); it( "Should update balances after transfers " , async () => { const initialOwnerBalance: bigint = await newToken.balanceOf(owner.address); // Transfer 100 tokens from owner to addr1. await newToken.transfer(addr1.address, 100 ); // Transfer another 50 tokens from owner to addr2. await newToken.transfer(addr2.address, 50 ); // Check balances. const finalOwnerBalance = await newToken.balanceOf(owner.address); expect(finalOwnerBalance).to.equal(initialOwnerBalance - 150 n) ; const addr1Balance = await newToken.balanceOf(addr1.address); expect(addr1Balance).to.equal( 100 ); const addr2Balance = await newToken.balanceOf(addr2.address); expect(addr2Balance).to.equal( 50 ); }); }); describe( "Minting" , () => { it( "It should mint tokens to the owner's address" , async () => { await newToken.mint(owner.address, 10 n * DECIMALS); const ownerBalance: bigint = await newToken.balanceOf(owner.address); expect(ownerBalance).to.equal( (INITIAL_SUPPLY + 10 n) * DECIMALS); }); }); describe( "Burning" , () => { it( "Should burn tokens from the owner's address" , async () => { await newToken.burn( 10 n * DECIMALS); const ownerBalance: bigint = await newToken.balanceOf(owner.address); expect(ownerBalance).to.equal((INITIAL_SUPPLY -10 n) * DECIMALS); }); }); describe( "Pauseable features" , () => { it( "Should pause the contract" , async () => { await newToken.pause(); expect( await newToken.paused()).to.be. true expect(newToken.transfer (addr1.address, 50 )).to.be.revertedWithCustomError; }); it( "Should unpause the contract" , async () => { await newToken.pause(); await newToken.unpause(); expect( await newToken.paused()).to.be. false await newToken.transfer(addr1.address, 50 ); const addr1Balance = await newToken.balanceOf(addr1.address); expect(addr1Balance).to.equal( 50 ); } ); }); describe( "Blacklist features" , () => { it( "Should add the address to the blacklist" , async () => { expect( await newToken.isBlackListed(addr1)).to.be. false ; await newToken.addBlackList(addr1); expect( await newToken.isBlackListed(addr1)).to.be.true; }); it( "Should remove the address from the blacklist" , async () => { await newToken.addBlackList(addr1); await (newToken.removeBlackList(addr1)); expect( await newToken.isBlackListed(addr1)) .to.be. false ; }); it( "Should prevent blacklisted address to transfer funds" , async () => { await newToken.transfer(addr1.address, 10 n * DECIMALS); await newToken.addBlackList(addr1) ; expect(newToken.connect(addr1).transfer(addr2.address, 50 )) .to.be.revertedWith( 'The sender address is blacklisted' ); }); it( "Should allow unblacklisted address to transfer funds" , async () => { await newToken.transfer(addr1.address, 10 n * DECIMALS); await newToken.addBlackList(addr1); await newToken.removeBlackList(addr1); await newToken.connect(addr1).transfer(addr2.address , 50 ); const addr2Balance = await newToken.balanceOf(addr2.address); expect(addr2Balance).to.equal( 50 ); }); }); });
脚本首先在下列位置部署合约UpgradeableToken1的第一个版本:
const oldToken = await upgrades. deployProxy (UpgradeableToken1, [owner.address], { initializer : 'initialize' , kind : 'transparent' });
然后透过部署第二个版本UpgradeableToken2 来升级合约:
newToken = await upgrades.upgradeProxy(oldToken, UpgradeableToken2, { kind: 'transparent' }) ;
值得注意的是,在生产环境中,使用UUPS 代理而不是透明代理可能会更明智。
结论
在本文中,我们探索了使用OpenZeppelin 的库和代理程序升级模式来创建可升级的ERC20 代币。透过提供初始版本和升级版本的程序代码片段和测试脚本,我们揭开了该过程的神秘面纱。
OpenZeppelin 的合约可升级分叉成为无缝合约演化的强大解决方案。我们的实践测试方法强调了智能合约开发中可靠性的重要性。
本文链接地址:https://www.wwsww.cn/ytf/25484.html
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。