如何编写您的 ERC20 代币





5.00/5 (2投票s)
使用 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();
}
我在合约中扩展了两个方法:mint
和 burn
。我们将需要它们来创建代币。毕竟,必须有人进行代币的初始生成。现在是时候进行测试了。但在此之前,我们需要确保我们的合约能够编译。
为此,我们需要使用 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日:初始版本