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);
});
});
});

