如何在以太坊上创建可升级的ERC-20智能合约

之前讨论了如何使用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
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。