合约第一版
这是向导为我产生的程序代码:
// 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 的合约可升级分叉成为无缝合约演化的强大解决方案。我们的实践测试方法强调了智能合约开发中可靠性的重要性。