65.9K
CodeProject 正在变化。 阅读更多。
Home

如何编写您的 ERC20 代币

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2024年1月2日

CPOL

5分钟阅读

viewsIcon

5997

downloadIcon

56

使用 hardhat 包和 TypeScript 创建 erc20 代币

引言

我想你们很多人已经知道什么是区块链了。在这篇文章中,我想告诉你关于 EVM 类区块链中的智能合约是什么。我们还将实现可能是最受欢迎的智能合约——ERC20 代币。我将向你展示如何创建一个简单的智能合约,为其编写测试并调用方法。

首先,让我们弄清楚什么是智能合约。智能合约本质上是一个包含函数和字段的类。发布在区块链上的智能合约已经是类的一个实例,由于你无法重启区块链,因此一旦发布,你也不能更改智能合约的实现。当然,你可以更改你的代码并重新发布合约,但它将是一个完全不同的智能合约。

与智能合约可以执行的操作有两种类型——读取操作和写入操作。读取操作是免费的,因为它们不会改变区块链,但写入需要付费。因为要改变区块链,必须创建一个交易,并且必须确认该交易。

让我们以最受欢迎的 ERC20 代币为例,更深入地了解智能合约。

事实上,任何实现了 EIP(Ethereum Improvement Proposals)中指定的特殊接口的智能合约都被视为 ERC20 代币 https://eips.ethereum.org/EIPS/eip-20

让我们来看看这个接口的方法构成,然后开始实现我们的 ERC20 代币。

  • function name() public view returns (string) - 返回代币名称的方法。它仅供 UI 使用,不应承担任何逻辑负载。调用此方法通常是免费的,因为它只读取区块链数据。如果你在最受欢迎的 USDT 代币(0xdAC17F958D2ee523a2206206994597C13D831ec7)上调用此方法,你将得到 “Tether USD”。
  • function symbol() public view returns (string) - 返回代币的符号。此值也仅供 UI 使用。
  • function decimals() public view returns (uint8) - 此方法返回代币的精度。一个非常重要的参数,例如,要发送一个精度为 2 的代币,你需要传入 100,如果你想发送一个精度为 6 的代币,那么你必须向发送函数发送 1000000。
  • function totalSupply() public view returns (uint256) - 返回已发行代币的总量。
  • function balanceOf(address _owner) public view returns (uint256 balance) - 返回指定区块链账户(作为 _owner 参数传递给合约)的代币余额。
  • function transfer(address _to, uint256 _value) public returns (bool success) - 将代币从一个钱包发送到另一个钱包的方法。当然,这是一个付费方法,因为它会改变区块链。我们必须在区块链中存储信息,即一个账户的余额减少了,另一个账户的余额增加了。
  • function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) - 非常有用的方法。使用它,代币可以由另一个钱包授权从你的账户中扣除。当然,你必须先批准此扣除。
  • function approve(address _spender, uint256 _value) public returns (bool success) - 批准另一个账户从你的账户扣除代币的方法。
  • function allowance(address _owner, address _spender) public view returns (uint256 remaining) - 使用此方法,你可以获取已批准扣除的总资金量。

此外,实现 erc20 接口的智能合约还必须发出标准事件。

  • event Transfer(address indexed _from, address indexed _to, uint256 _value) - 代币从一个账户转移到另一个账户的事件。
  • event Approval(address indexed _owner, address indexed _spender, uint256 _value) - 已批准扣款的事件。

至此,我们弄清楚了 ERC20 代币应该是什么。让我们开始实现。为此,我将使用 hardhat 包和 typescript 语言。创建项目文件夹后,我将安装以下包:

npm i -D hardhat typescript

之后,我们在 contracts 文件夹中创建一个名为 IERC20.sol 的接口文件。

pragma solidity ^0.8.20;

interface IERC20 {
   function name() external view returns (string memory);
   function symbol() external view returns (string memory);
   function decimals() external view returns (uint8);
   function totalSupply() external view returns (uint256);
   function balanceOf(address owner) external view returns (uint256 balance);
   function transfer(address to, uint256 value) external returns (bool success);
   function transferFrom(address _from, address _to, uint256 _value) 
                         external returns (bool success);
   function approve(address spender, uint256 value) external returns (bool success);
   function allowance(address owner, address spender) external view returns 
                     (uint256 remaining);


   event Transfer(address indexed from, address indexed to, uint256 value);
   event Approval(address indexed owner, address indexed spender, uint256 value);
}

之后,让我们开始创建合约。为此,我们将创建一个新文件 ERC20.sol。在其中,我们将创建一个实现 IERC20 接口的 ERC20 合约。我们将草拟必要的方法。为了让我们的代码能够编译,我们在每个方法中返回 NotImplementedError

pragma solidity ^0.8.20;

import {IERC20} from "./IERC20.sol";

contract ERC20 is IERC20{

   constructor(string memory name, string memory symbol, uint8 decimals){
   }

   function name() public view returns (string memory){
       revert NotImplementedError();
   }

   function symbol() public view returns (string memory){
       revert NotImplementedError();
   }

   function decimals() public view returns (uint8){
       revert NotImplementedError();
   }

   function totalSupply() public view returns (uint256){
       revert NotImplementedError();
   }

   function balanceOf(address owner) public view returns (uint256){
       revert NotImplementedError();
   }

   function transfer(address to, uint256 value) public returns (bool){
       revert NotImplementedError();
   }

   function transferFrom(address from, address to, uint256 amount) public returns (bool){
       revert NotImplementedError();
   }

   function approve(address spender, uint256 amount) public returns (bool){
       revert NotImplementedError();
   }

   function allowance(address owner, address spender) public view returns (uint256){
       revert NotImplementedError();
   }

   function mint(address account, uint256 amount) public returns (bool success){
       revert NotImplementedError();
   }

   function burn(address account, uint256 amount) public returns (bool success){
       revert NotImplementedError();
   }

   error NotImplementedError();
}

我在合约中扩展了两个方法:mintburn。我们将需要它们来创建代币。毕竟,必须有人进行代币的初始生成。现在是时候进行测试了。但在此之前,我们需要确保我们的合约能够编译。

为此,我们需要使用 npx hardhat compile 命令。为了使此命令正常工作,我们需要添加配置文件 hardhat.config.ts

我们还需要另外两个包来编写测试:

npm i -D @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-ethers

接下来,我创建了一个名为 test/ERC20.spec.ts 的文件并在其中实现了测试。

import { expect } from "chai";
import { ethers } from "hardhat";
import {HardhatEthersSigner, SignerWithAddress} from 
        "@nomicfoundation/hardhat-ethers/signers";
import { ERC20 } from "../typechain-types";
import {ContractFactory} from "ethers";

const ZERO_ADDRESS : string = "0x0000000000000000000000000000000000000000";

describe("Erc20 contract", () => {
   let accounts : HardhatEthersSigner[];

   let erc20Contract : ERC20;

   const name : string = "MyToken";
   const symbol : string = "MT";
   const decimals : number = 18;

   beforeEach(async () =>{
       accounts = await ethers.getSigners();

       const erc20Factory: ContractFactory  = await ethers.getContractFactory('ERC20');
       erc20Contract = (await erc20Factory.deploy(name, symbol, decimals)) as ERC20;
   });

   describe ("deployment", () => {
       it("Should set the right name", async () => {
           expect(await erc20Contract.name()).to.equal(name);
       });

       it("Should set the right symbol", async () => {
           expect(await erc20Contract.symbol()).to.equal(symbol);
       });

       it("Should set the right decimals", async () => {
           expect(await erc20Contract.decimals()).to.equal(decimals);
       });

       it("Should set zero total supply", async () => {
           expect(await erc20Contract.totalSupply()).to.equal(0);
       });
   });

   describe ("mint", () => {
       it("Shouldn't be possible mint to zero address", async () => {
           const mintAmount = 1;
           await expect(erc20Contract.mint(ZERO_ADDRESS, mintAmount))
               .to.be.revertedWith("account shouldn't be zero");
       });

       it("Shouldn't be possible mint zero amount", async () => {
           const mintAmount = 0;
           await expect(erc20Contract.mint(accounts[0].address, mintAmount))
               .to.be.revertedWith("amount shouldn't be zero");
       });

       it("Should be change balance", async () =>{
           const mintAmount = 10;
           await erc20Contract.mint(accounts[0].address, mintAmount);
           expect(await erc20Contract.balanceOf(accounts[0].address)).to.equal(mintAmount);
       });

       it("Should be change total supply", async () =>{
           const mintAmount1 = 1;
           const mintAmount2 = 2;
           await erc20Contract.mint(accounts[0].address, mintAmount1);
           await erc20Contract.mint(accounts[1].address, mintAmount2);
           expect(await erc20Contract.totalSupply()).to.equal(mintAmount1 + mintAmount2);
       });
   });

   describe("transfer", () => {
       it("Shouldn't be possible transfer to zero address", async () =>{
           const from : SignerWithAddress = accounts[0];
           const toAddress : string = ZERO_ADDRESS;
           const transferAmount : number = 1;
           await expect(erc20Contract.connect(from).transfer(toAddress, transferAmount))
               .to.be.revertedWith("to address shouldn't be zero");
       });

       it("Shouldn't be possible transfer zero amount", async () =>{
           const from : SignerWithAddress = accounts[0];
           const toAddress : string = accounts[1].address;
           const transferAmount : number = 0;
           await expect(erc20Contract.connect(from).transfer(toAddress, transferAmount))
               .to.be.revertedWith("amount shouldn't be zero");
       });

       it("Shouldn't be possible transfer more than account balance", async () =>{
           const from : SignerWithAddress = accounts[0];
           const toAddress: string = accounts[1].address;
           const mintAmount: number = 1;
           await erc20Contract.mint(from.address, mintAmount);


           await expect(erc20Contract.connect(from).transfer(toAddress, mintAmount + 1))
               .to.be.reverted;
       });

       it("Shouldn't change total supply", async () => {
           const from: SignerWithAddress = accounts[0];
           const toAddress: string = accounts[1].address;
           const mintAmount: number = 1;
           await erc20Contract.mint(from.address, mintAmount);
           await erc20Contract.connect(from).transfer(toAddress, mintAmount);
           expect(await erc20Contract.totalSupply()).to.equal(mintAmount);
       });

       it("Should increase balance", async () => {
           const from : SignerWithAddress = accounts[0];
           const toAddress: string = accounts[1].address;
           const mintAmount : number = 1;
           await erc20Contract.mint(from.address, mintAmount);
           await erc20Contract.connect(from).transfer(toAddress, mintAmount);
           expect(await erc20Contract.balanceOf(toAddress)).to.equal(mintAmount);
       });

       it("Should decrease balance", async () => {
           const from : SignerWithAddress = accounts[0];
           const toAddress : string = accounts[1].address;
           const mintAmount : number = 1;
           await erc20Contract.mint(from.address, mintAmount);
           await erc20Contract.connect(from).transfer(toAddress, mintAmount);
           expect(await erc20Contract.balanceOf(from.address)).to.equal(0);
       });
   });

   describe ("approve", () => {
       it("Shouldn't be possible to zero address", async () => {
           const amount = 1;
           await expect(erc20Contract.connect(accounts[0]).approve(ZERO_ADDRESS, amount))
               .to.be.revertedWith("spender address shouldn't be zero");
       });

       it("Shouldn't be possible zero amount", async () => {
           const amount = 0;
           await expect(erc20Contract.connect(accounts[0]).approve
                       (accounts[1].address, amount))
               .to.be.revertedWith("amount shouldn't be zero");
       });

       it("Should be change allowance", async () =>{
           const amount = 1;
           await erc20Contract.connect(accounts[0]).approve(accounts[1].address, amount);
           expect(await erc20Contract.allowance
                 (accounts[0].address, accounts[1].address)).to.equal(amount);
       });
   });

   describe("transferFrom", () => {
       it("Shouldn't be possible more than allowance", async () =>{
           const allowanceAmount : number = 1;
           const transferAmount : number = allowanceAmount + 1;
           await erc20Contract.connect(accounts[0]).approve
                                      (accounts[1].address, allowanceAmount);
           await expect(erc20Contract.connect(accounts[1]).transferFrom
                       (accounts[0].address, accounts[2].address, transferAmount))
               .to.be.revertedWith("insufficient allowance funds");
       });

       it("Should spend allowance", async () =>{
           const allowanceAmount : number = 2;
           const transferAmount : number = allowanceAmount - 1;
           await erc20Contract.mint(accounts[0].address, allowanceAmount);
           await erc20Contract.connect(accounts[0]).approve
                                      (accounts[1].address, allowanceAmount);
           await erc20Contract.connect(accounts[1]).transferFrom
                          (accounts[0].address, accounts[2].address, transferAmount);
           expect(await erc20Contract.allowance
                 (accounts[0].address, accounts[1].address)).to.equal
                 (allowanceAmount - transferAmount);
       });

       it("Should increase balance", async () =>{
           const allowanceAmount : number = 2;
           const transferAmount : number = allowanceAmount - 1;
           await erc20Contract.mint(accounts[0].address, allowanceAmount);
           await erc20Contract.connect(accounts[0]).approve(accounts[1].address, 
                                       allowanceAmount);
           await erc20Contract.connect(accounts[1]).transferFrom
                 (accounts[0].address, accounts[2].address, transferAmount);
           expect(await erc20Contract.balanceOf(accounts[2].address)).to.equal
                 (transferAmount);
       });

       it("Should decrease balance", async () =>{
           const allowanceAmount : number = 2;
           const transferAmount : number = allowanceAmount - 1;
           await erc20Contract.mint(accounts[0].address, allowanceAmount);
           await erc20Contract.connect(accounts[0]).approve
                                      (accounts[1].address, allowanceAmount);
           await erc20Contract.connect(accounts[1]).transferFrom
                      (accounts[0].address, accounts[2].address, transferAmount);
           expect(await erc20Contract.balanceOf(accounts[0].address)).to.equal
                      (allowanceAmount - transferAmount);
       });
   });

   describe ("burn", () => {
       it("Shouldn't be possible burn from zero address", async () => {
           const amount = 1;
           await expect(erc20Contract.burn(ZERO_ADDRESS, amount))
               .to.be.revertedWith("account shouldn't be zero");
       });

       it("Shouldn't be possible burn zero amount", async () => {
           const amount = 0;
           await expect(erc20Contract.burn(accounts[0].address, amount))
               .to.be.revertedWith("amount shouldn't be zero");
       });

       it("Should be change balance", async () =>{
           const mintAmount : number = 2;
           const burnAmount : number = 1;
           await erc20Contract.mint(accounts[0].address, mintAmount);
           await erc20Contract.burn(accounts[0].address, burnAmount);
           expect(await erc20Contract.balanceOf(accounts[0].address)).to.equal
                 (mintAmount - burnAmount);
       });

       it("Should be change total supply", async () =>{
           const mintAmount : number = 2;
           const burnAmount : number = 1;
           await erc20Contract.mint(accounts[0].address, mintAmount);
           await erc20Contract.burn(accounts[0].address, burnAmount);
           expect(await erc20Contract.totalSupply()).to.equal(mintAmount - burnAmount);
       });

       it("Should burn all balance", async () =>{
           const mintAmount : number = 2;
           const burnAmount : number = mintAmount + 1;
           await erc20Contract.mint(accounts[0].address, mintAmount);
           await erc20Contract.burn(accounts[0].address, burnAmount);
           expect(await erc20Contract.balanceOf(accounts[0].address)).to.equal(0);
       });
   });
});

测试使用 npx hardhat test 命令启动,要查看覆盖率,你需要运行 npx hardhat coverage 命令。现在,当然,所有测试都是红色的,因为我们还没有实现任何方法。

最后,我得到了这个实现:

pragma solidity ^0.8.20;

import {IERC20} from "./IERC20.sol";

contract ERC20 is IERC20{

   mapping(address => uint256) private _balances;
   mapping(address => mapping(address => uint256)) private _allowances;

   uint256 private _totalSupply;

   string private _name;
   string private _symbol;
   uint8 private _decimals;

   constructor(string memory name, string memory symbol, uint8 decimals){
       _name = name;
       _symbol = symbol;
       _decimals = decimals;
   }

   function name() public view returns (string memory){
       return _name;
   }

   function symbol() public view returns (string memory){
       return _symbol;
   }

   function decimals() public view returns (uint8){
       return _decimals;
   }

   function totalSupply() public view returns (uint256){
       return _totalSupply;
   }

   function balanceOf(address owner) public view returns (uint256){
       return _balances[owner];
   }

   function transfer(address to, uint256 value) public returns (bool){
       _transfer(msg.sender, to, value);
       return true;
   }

   function transferFrom(address from, address to, uint256 amount) public returns (bool){
       _spendAllowance(from, msg.sender, amount);
       _transfer(from, to, amount);
       return true;
   }

   function approve(address spender, uint256 amount) public returns (bool){
       _approve(msg.sender, spender, amount);
       return true;
   }

   function allowance(address owner, address spender) public view returns (uint256){
       return _allowances[owner][spender];
   }

   function mint(address account, uint256 amount) public{
       _mint(account, amount);
   }

   function burn(address account, uint256 amount) public{
       _burn(account, amount);
   }

   function _transfer(address from, address to, uint256 amount) internal {
       require(to != address(0), "to address shouldn't be zero");
       require(amount != 0, "amount shouldn't be zero");

       uint256 fromBalance = _balances[from];
       require(fromBalance >= amount, "insufficient funds");
       _balances[from] = fromBalance - amount;
       _balances[to] += amount;
       emit Transfer(from, to, amount);
   }

   function _mint(address account, uint256 amount) internal {
       require(account != address(0), "account shouldn't be zero");
       require(amount != 0, "amount shouldn't be zero");

       _totalSupply += amount;
       _balances[account] += amount;
       emit Transfer(address(0), account, amount);
   }

   function _burn(address account, uint256 amount) internal virtual {
       require(account != address(0), "account shouldn't be zero");
       require(amount != 0, "amount shouldn't be zero");

       uint256 accountBalance = _balances[account];
       uint256 burnAmount = amount>accountBalance ? accountBalance : amount;
       _balances[account] = accountBalance - burnAmount;
       _totalSupply -= burnAmount;

       emit Transfer(account, address(0), burnAmount);
   }

   function _approve(address owner, address spender, uint256 amount) internal {
       require(spender != address(0), "spender address shouldn't be zero");
       require(amount != 0, "amount shouldn't be zero");

       _allowances[owner][spender] = amount;
       emit Approval(owner, spender, amount);
   }

   function _spendAllowance(address owner, address spender, uint256 amount) internal {
       uint256 currentAllowance = allowance(owner, spender);
       require(currentAllowance >= amount, "insufficient allowance funds");
       _approve(owner, spender, currentAllowance - amount);
   }
}

如果我们运行测试,它们都会变成绿色,这意味着我们的实现是正确的。希望这篇文章能帮助你开始编写你的智能合约。你可以在 https://github.com/waksund/erc20 找到源代码。

历史

  • 2024年1月2日:初始版本
© . All rights reserved.