# ERC20 Test **Published by:** [xixixueling](https://paragraph.com/@xixixueling-2/) **Published on:** 2025-06-25 **URL:** https://paragraph.com/@xixixueling-2/erc20-test ## Content https://hardhat.org/tutorial/writing-and-compiling-contracts 用solidity写一个简单的合约,用hardhat完成测试的过程,以及一些小的知识点的说明ERC-20合约介绍其核心作用,确保代币在钱包,交易所,dapp之间的胡操作性 必须实现六个核心的函数totalSupply() 返回代币总供应量balanceOf(address) 查询制定地址余额transfer(address, uint256) 从调用者地址转账到目标地址allowance(address, address) 查询授权额度approve(address, uint256) 授权第三方地址使用一定数量的代币transferFrom(address, address, uint256) 被授权者从所有者地址转账代币一个简单的合约代码//SPDX-License-Identifier: UNLICENSED // Solidity files have to start with this pragma. // It will be used by the Solidity compiler to validate its version. pragma solidity ^0.8.0; import "hardhat/console.sol"; // This is the main building block for smart contracts. contract Token { // Some string type variables to identify the token. string public name = "My Hardhat Token"; string public symbol = "MHT"; uint8 public decimals = 18; // The fixed amount of tokens, stored in an unsigned integer type variable. uint256 public totalSupply = 1000000 * 10**18; // 1,000,000 tokens with 18 decimals // An address type variable is used to store ethereum accounts. address public owner; // A mapping is a key/value map. Here we store each account's balance. mapping(address => uint256) balances; // A mapping to store allowances for the approve/transferFrom pattern mapping(address => mapping(address => uint256)) allowances; // The Transfer event helps off-chain applications understand // what happens within your contract. event Transfer(address indexed _from, address indexed _to, uint256 _value); // The Approval event is emitted when approve is called event Approval(address indexed _owner, address indexed _spender, uint256 _value); /** * Contract initialization. */ constructor() { // The totalSupply is assigned to the transaction sender, which is the // account that is deploying the contract. balances[msg.sender] = totalSupply; owner = msg.sender; // Emit transfer event for the initial allocation emit Transfer(address(0), msg.sender, totalSupply); } /** * A function to transfer tokens. * * The `external` modifier makes a function *only* callable from *outside* * the contract. */ function transfer(address to, uint256 amount) external returns (bool) { // Check if the transaction sender has enough tokens. // If `require`'s first argument evaluates to `false`, the // transaction will revert. require(balances[msg.sender] >= amount, "Not enough tokens"); require(to != address(0), "Cannot transfer to zero address"); console.log("Transferring %s tokens from %s to %s", amount, msg.sender, to); // Transfer the amount. balances[msg.sender] -= amount; balances[to] += amount; // Notify off-chain applications of the transfer. emit Transfer(msg.sender, to, amount); return true; } /** * Read only function to retrieve the token balance of a given account. * * The `view` modifier indicates that it doesn't modify the contract's * state, which allows us to call it without executing a transaction. */ function balanceOf(address account) external view returns (uint256) { return balances[account]; } /** * Approve function to allow spender to spend tokens on behalf of owner */ function approve(address spender, uint256 amount) external returns (bool) { require(spender != address(0), "Cannot approve zero address"); allowances[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } /** * TransferFrom function to transfer tokens from one address to another * using the allowance mechanism */ function transferFrom(address from, address to, uint256 amount) external returns (bool) { require(balances[from] >= amount, "Not enough tokens"); require(allowances[from][msg.sender] >= amount, "Insufficient allowance"); require(to != address(0), "Cannot transfer to zero address"); balances[from] -= amount; balances[to] += amount; allowances[from][msg.sender] -= amount; emit Transfer(from, to, amount); return true; } /** * Get the allowance for a spender */ function allowance(address tokenOwner, address spender) external view returns (uint256) { return allowances[tokenOwner][spender]; } } 关于event的说明链下应用集成钱包,交易所,dapp等监听转账事件,更新钱包余额审计和追踪 交易记录授权记录Hardhat实现测试合约测试点合约部署测试 ✅ 验证总供应量正确分配给部署者基础转账功能测试 ✅ 成功转账代币 ✅ 多账户间转账 ✅ Transfer 事件触发 ✅ 余额不足时失败 ✅ 向零地址转账失败授权和委托转账功能测试 ✅ 成功授权代币 ✅ Approval 事件触发 ✅ 向零地址授权失败 ✅ 使用 transferFrom 委托转账 ✅ 授权额度不足时失败 ✅ 余额不足时 transferFrom 失败 ✅ transferFrom 向零地址失败 ✅ transferFrom 触发 Transfer 事件授权额度管理测试 ✅ 正确返回授权额度 ✅ transferFrom 后授权额度减少 ✅ 多次授权覆盖之前额度边界条件测试(新增) ✅ 转账 0 个代币 ✅ 授权 0 个代币 ✅ 转账全部余额import { expect } from "chai"; import { ethers } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; describe("Token 合约测试", function () { // 部署合约的通用函数 async function deployTokenFixture() { const [owner, addr1, addr2] = await ethers.getSigners(); const token = await ethers.deployContract("Token"); await token.waitForDeployment(); return { token, owner, addr1, addr2 }; } // ==================== 部署测试 ==================== describe("合约部署", function () { it("应该将总供应量分配给部署者", async function () { const { token, owner } = await loadFixture(deployTokenFixture); const ownerBalance = await token.balanceOf(owner.address); expect(await token.totalSupply()).to.equal(ownerBalance); }); }); // ==================== 基础转账功能测试 ==================== describe("基础转账功能", function () { it("应该能够成功转账代币", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); // 执行转账 await token.transfer(addr1.address, 100); // 验证转账结果 expect(await token.balanceOf(addr1.address)).to.equal(100n); expect(await token.balanceOf(owner.address)).to.equal((await token.totalSupply()) - 100n); }); it("应该能够进行多账户间的转账", async function () { const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture); // 第一步:owner 转给 addr1 await token.transfer(addr1.address, 100); // 第二步:addr1 转给 addr2 await token.connect(addr1).transfer(addr2.address, 50); // 验证最终余额 expect(await token.balanceOf(addr1.address)).to.equal(50n); expect(await token.balanceOf(addr2.address)).to.equal(50n); expect(await token.balanceOf(owner.address)).to.equal((await token.totalSupply()) - 100n); }); it("应该触发 Transfer 事件", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); // 验证转账事件 await expect(token.transfer(addr1.address, 100)) .to.emit(token, "Transfer") .withArgs(owner.address, addr1.address, 100n); }); it("当余额不足时应该失败", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); const initialOwnerBalance = await token.balanceOf(owner.address); // 尝试转出超过余额的代币 await expect(token.connect(addr1).transfer(owner.address, 1)) .to.be.revertedWith("Not enough tokens"); // 验证余额没有变化 expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance); expect(await token.balanceOf(addr1.address)).to.equal(0n); }); it("向零地址转账应该失败", async function () { const { token, owner } = await loadFixture(deployTokenFixture); // 尝试向零地址转账 await expect(token.transfer("0x0000000000000000000000000000000000000000", 100)) .to.be.revertedWith("Cannot transfer to zero address"); }); }); // ==================== 授权和委托转账功能测试 ==================== describe("授权和委托转账功能", function () { it("应该能够成功授权代币", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); // 执行授权 await token.approve(addr1.address, 100); // 验证授权额度 expect(await token.allowance(owner.address, addr1.address)).to.equal(100n); }); it("应该触发 Approval 事件", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); // 验证授权事件 await expect(token.approve(addr1.address, 100)) .to.emit(token, "Approval") .withArgs(owner.address, addr1.address, 100n); }); it("向零地址授权应该失败", async function () { const { token, owner } = await loadFixture(deployTokenFixture); // 尝试向零地址授权 await expect(token.approve("0x0000000000000000000000000000000000000000", 100)) .to.be.revertedWith("Cannot approve zero address"); }); it("应该能够使用 transferFrom 进行委托转账", async function () { const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture); // 第一步:owner 授权给 addr1 await token.approve(addr1.address, 100); // 第二步:addr1 使用 transferFrom 转出 owner 的代币 await token.connect(addr1).transferFrom(owner.address, addr2.address, 50); // 验证转账结果 expect(await token.balanceOf(addr2.address)).to.equal(50n); expect(await token.balanceOf(owner.address)).to.equal((await token.totalSupply()) - 50n); // 验证授权额度减少 expect(await token.allowance(owner.address, addr1.address)).to.equal(50n); }); it("当授权额度不足时 transferFrom 应该失败", async function () { const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture); // 授权额度不足 await token.approve(addr1.address, 50); // 尝试转出超过授权额度的代币 await expect(token.connect(addr1).transferFrom(owner.address, addr2.address, 100)) .to.be.revertedWith("Insufficient allowance"); }); it("当余额不足时 transferFrom 应该失败", async function () { const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture); // 给 addr1 一些代币 await token.transfer(addr1.address, 50); // addr1 授权给 addr2 await token.connect(addr1).approve(addr2.address, 100); // addr2 尝试转出超过 addr1 余额的代币 await expect(token.connect(addr2).transferFrom(addr1.address, owner.address, 100)) .to.be.revertedWith("Not enough tokens"); }); it("transferFrom 向零地址转账应该失败", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); // 先授权 await token.approve(addr1.address, 100); // 尝试向零地址转账 await expect(token.connect(addr1).transferFrom(owner.address, "0x0000000000000000000000000000000000000000", 50)) .to.be.revertedWith("Cannot transfer to zero address"); }); it("transferFrom 应该触发 Transfer 事件", async function () { const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture); // 先授权 await token.approve(addr1.address, 100); // 验证 transferFrom 事件 await expect(token.connect(addr1).transferFrom(owner.address, addr2.address, 50)) .to.emit(token, "Transfer") .withArgs(owner.address, addr2.address, 50n); }); }); // ==================== 授权额度管理测试 ==================== describe("授权额度管理", function () { it("应该正确返回授权额度", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); // 初始授权额度应该为 0 expect(await token.allowance(owner.address, addr1.address)).to.equal(0n); // 授权后额度应该更新 await token.approve(addr1.address, 100); expect(await token.allowance(owner.address, addr1.address)).to.equal(100n); }); it("transferFrom 后授权额度应该正确减少", async function () { const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture); // 初始授权 await token.approve(addr1.address, 100); expect(await token.allowance(owner.address, addr1.address)).to.equal(100n); // 使用部分授权额度 await token.connect(addr1).transferFrom(owner.address, addr2.address, 30); // 验证授权额度减少 expect(await token.allowance(owner.address, addr1.address)).to.equal(70n); }); it("多次授权应该覆盖之前的授权额度", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); // 第一次授权 await token.approve(addr1.address, 100); expect(await token.allowance(owner.address, addr1.address)).to.equal(100n); // 第二次授权(覆盖) await token.approve(addr1.address, 200); expect(await token.allowance(owner.address, addr1.address)).to.equal(200n); }); }); // ==================== 边界条件测试 ==================== describe("边界条件测试", function () { it("应该能够转账 0 个代币", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); const initialBalance = await token.balanceOf(addr1.address); // 转账 0 个代币 await token.transfer(addr1.address, 0); // 余额应该不变 expect(await token.balanceOf(addr1.address)).to.equal(initialBalance); }); it("应该能够授权 0 个代币", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); // 授权 0 个代币 await token.approve(addr1.address, 0); // 授权额度应该为 0 expect(await token.allowance(owner.address, addr1.address)).to.equal(0n); }); it("应该能够转账全部余额", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); const totalSupply = await token.totalSupply(); // 转账全部余额 await token.transfer(addr1.address, totalSupply); // 验证转账结果 expect(await token.balanceOf(addr1.address)).to.equal(totalSupply); expect(await token.balanceOf(owner.address)).to.equal(0n); }); }); }); ## Publication Information - [xixixueling](https://paragraph.com/@xixixueling-2/): Publication homepage - [All Posts](https://paragraph.com/@xixixueling-2/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@xixixueling-2): Subscribe to updates