目录
VS Code + Hardhat 智能合约开发完整指南
第一章:环境搭建与配置
1.1 系统要求检查
1.2 VS Code 安装与配置
1.2.1 安装 VS Code
1.2.2 安装必备扩展
1.2.3 VS Code 工作区设置
第二章:Hardhat 项目初始化
2.1 创建新项目
2.2 项目结构
2.3 配置文件详解
2.3.1 Hardhat 配置文件
2.3.2 TypeScript 配置
2.3.3 ESLint 配置
2.3.4 Prettier 配置
2.3.5 环境变量配置
第三章:智能合约开发
3.1 创建智能合约
3.1.1 基础合约示例
3.1.2 接口定义
3.1.3 库合约
3.2 编译合约
3.3 编译配置文件
第四章:测试开发
4.1 测试环境配置
4.1.1 测试配置文件
4.1.2 基础测试文件
4.1.3 集成测试
4.2 运行测试
4.3 测试覆盖率
第五章:部署脚本
5.1 基础部署脚本
5.2 多网络部署脚本
5.3 升级部署脚本
5.4 运行部署脚本
第六章:交互与调试
6.1 Hardhat Console 使用
6.2 调试交易
6.3 交互脚本
第七章:自动化与工作流
7.1 npm 脚本配置
7.2 Git Hooks 配置
7.3 CI/CD 配置
第八章:高级功能与优化
8.1 自定义 Hardhat 任务
8.2 安全分析配置
8.3 性能优化
8.3.1 编译优化
8.3.2 测试优化
第九章:故障排除与调试
9.1 常见问题解决
9.1.1 编译错误
9.1.2 测试失败
9.1.3 部署失败
9.1.4 验证失败
9.2 调试技巧
9.2.1 使用 console.log
9.2.2 使用 Hardhat Network 日志
9.2.3 交易追踪
第十章:最佳实践总结
10.1 项目结构最佳实践
10.2 代码质量最佳实践
10.2.1 Solidity 编码规范
10.2.2 安全最佳实践
10.3 部署最佳实践
10.3.1 多阶段部署
10.3.2 紧急情况处理
10.4 监控与维护
10.4.1 事件监听
10.4.2 定期检查脚本
总结
我们正在使用VS Code和Hardhat进行智能合约开发。本指南将涵盖从环境设置到部署的完整流程。 注意:本指南假设你已安装Node.js(推荐版本v16或更高)和VS Code。
步骤概览:
环境准备
初始化Hardhat项目
配置VS Code
编写智能合约
编译合约
测试合约
部署合约
验证合约
让我们开始吧!
VS Code + Hardhat 智能合约开发完整指南
第一章:环境搭建与配置
1.1 系统要求检查
# 检查 Node.js 版本
node –version
# 需要 v16.0.0 或更高版本
# 检查 npm 版本
npm –version
# 需要 v7.0.0 或更高版本
# 推荐使用 nvm 管理 Node.js 版本
# macOS/Linux 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# Windows 安装 nvm-windows
# 下载地址:https://github.com/coreybutler/nvm-windows/releases
# 使用 nvm 安装 Node.js
nvm install 18
nvm use 18
1.2 VS Code 安装与配置
1.2.1 安装 VS Code
-
下载地址:https://code.visualstudio.com/
-
选择对应系统版本下载安装
1.2.2 安装必备扩展
在 VS Code 扩展市场安装以下插件:
// .vscode/extensions.json – 团队共享配置
{
"recommendations": [
// Solidity 开发核心扩展
"JuanBlanco.solidity", // Solidity 语言支持
"NomicFoundation.hardhat-solidity", // Hardhat 集成
"trufflesuite.truffle-vscode", // Truffle 集成(可选)
// 开发工具
"dbaeumer.vscode-eslint", // ESLint 集成
"esbenp.prettier-vscode", // Prettier 代码格式化
"eamodio.gitlens", // Git 增强
"ms-vscode.test-adapter-converter", // 测试适配器
// AI 辅助编程(可选但推荐)
"GitHub.copilot", // GitHub Copilot
"Codeium.codeium", // Codeium AI 助手
// 其他实用工具
"streetsidesoftware.code-spell-checker", // 拼写检查
"mikestead.dotenv", // .env 文件支持
"ms-azuretools.vscode-docker", // Docker 支持
"ms-vscode-remote.remote-ssh" // 远程开发
]
}
1.2.3 VS Code 工作区设置
// .vscode/settings.json
{
// 文件排除
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/build": true,
"**/coverage": true,
"**/cache": true
},
// Solidity 配置
"solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404",
"solidity.defaultCompiler": "remote",
"solidity.enableLocalNodeCompiler": false,
// 编辑器通用设置
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
// 文件类型关联
"files.associations": {
"*.sol": "solidity",
".env*": "properties"
},
// Solidity 特定设置
"[solidity]": {
"editor.defaultFormatter": "JuanBlanco.solidity",
"editor.formatOnSave": true,
"editor.tabSize": 4
},
// TypeScript/JavaScript 设置
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 终端设置
"terminal.integrated.defaultProfile.windows": "PowerShell",
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.defaultProfile.osx": "zsh",
// 工作区设置
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "Default Dark+",
// Git 设置
"git.autofetch": true,
"git.confirmSync": false,
"git.enableSmartCommit": true
}
第二章:Hardhat 项目初始化
2.1 创建新项目
# 1. 创建项目目录
mkdir my-hardhat-project
cd my-hardhat-project
# 2. 初始化 npm 项目
npm init -y
# 3. 安装 Hardhat
npm install –save-dev hardhat
# 4. 初始化 Hardhat 项目
npx hardhat init
# 选择项目类型:
# ❯ Create a TypeScript project (推荐)
# Create a JavaScript project
# Create an empty hardhat.config.js
# 5. 安装依赖包
npm install –save-dev @nomicfoundation/hardhat-toolbox
npm install –save-dev @typechain/hardhat @typechain/ethers-v5 typechain
npm install –save-dev dotenv
# 6. 安装 OpenZeppelin 合约库
npm install @openzeppelin/contracts
# 7. 安装开发工具
npm install –save-dev eslint prettier eslint-config-prettier
npm install –save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
2.2 项目结构
my-hardhat-project/
├── contracts/ # Solidity 智能合约
│ ├── interfaces/ # 接口定义
│ ├── libraries/ # 库合约
│ ├── tokens/ # 代币合约
│ └── utils/ # 工具合约
├── scripts/ # 部署和交互脚本
│ ├── deploy/ # 部署脚本
│ ├── upgrade/ # 升级脚本
│ └── tasks/ # Hardhat 自定义任务
├── test/ # 测试文件
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── fixtures/ # 测试夹具
├── deployments/ # 部署记录(hardhat-deploy)
├── typechain-types/ # TypeScript 类型定义
├── artifacts/ # 编译产物
├── cache/ # 缓存文件
├── .vscode/ # VS Code 配置
├── .env # 环境变量
├── .env.example # 环境变量示例
├── hardhat.config.ts # Hardhat 配置
├── tsconfig.json # TypeScript 配置
├── package.json # 项目依赖
└── README.md # 项目说明
2.3 配置文件详解
2.3.1 Hardhat 配置文件
// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@typechain/hardhat";
import "@nomiclabs/hardhat-ethers";
import "@nomiclabs/hardhat-waffle";
import "@nomiclabs/hardhat-etherscan";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "dotenv/config";
const config: HardhatUserConfig = {
// Solidity 编译器配置
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
// 通过 IR 编译(可选,可减少字节码大小)
viaIR: process.env.VIA_IR === "true",
// 输出详细编译信息
outputSelection: {
"*": {
"*": ["abi", "evm.bytecode", "evm.deployedBytecode", "evm.methodIdentifiers"],
"": ["ast"]
}
}
},
},
// 网络配置
networks: {
// 本地开发网络
hardhat: {
chainId: 31337,
// 配置本地分叉(可选)
forking: {
url: process.env.ETH_MAINNET_URL || "",
blockNumber: 17570000,
enabled: process.env.FORKING_ENABLED === "true",
},
// 矿工配置
mining: {
auto: true,
interval: 5000, // 区块生成间隔(毫秒)
},
// 初始账户配置
accounts: {
mnemonic: process.env.MNEMONIC || "test test test test test test test test test test test junk",
count: 20,
accountsBalance: "10000000000000000000000", // 10 ETH
},
},
// 本地节点网络
localhost: {
url: "http://127.0.0.1:8545",
chainId: 31337,
timeout: 60000,
},
// 测试网络
goerli: {
url: process.env.GOERLI_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 5,
gasPrice: "auto",
gasMultiplier: 1.2,
timeout: 60000,
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 11155111,
},
// Layer 2 网络
polygon: {
url: process.env.POLYGON_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 137,
},
arbitrum: {
url: process.env.ARBITRUM_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 42161,
},
optimism: {
url: process.env.OPTIMISM_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 10,
},
// 主网(谨慎使用)
mainnet: {
url: process.env.MAINNET_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 1,
gasPrice: process.env.MAINNET_GAS_PRICE
? parseInt(process.env.MAINNET_GAS_PRICE)
: "auto",
},
},
// Etherscan 验证配置
etherscan: {
apiKey: {
mainnet: process.env.ETHERSCAN_API_KEY || "",
goerli: process.env.ETHERSCAN_API_KEY || "",
sepolia: process.env.ETHERSCAN_API_KEY || "",
polygon: process.env.POLYGONSCAN_API_KEY || "",
polygonMumbai: process.env.POLYGONSCAN_API_KEY || "",
arbitrumOne: process.env.ARBISCAN_API_KEY || "",
arbitrumGoerli: process.env.ARBISCAN_API_KEY || "",
optimism: process.env.OPTIMISM_API_KEY || "",
optimismGoerli: process.env.OPTIMISM_API_KEY || "",
},
customChains: [
{
network: "arbitrumGoerli",
chainId: 421613,
urls: {
apiURL: "https://api-goerli.arbiscan.io/api",
browserURL: "https://goerli.arbiscan.io/"
}
}
]
},
// Gas 报告配置
gasReporter: {
enabled: process.env.REPORT_GAS === "true",
currency: "USD",
coinmarketcap: process.env.COINMARKETCAP_API_KEY || "",
token: "ETH",
gasPrice: 21,
excludeContracts: ["mocks/", "test/"],
src: "./contracts",
},
// 测试配置
mocha: {
timeout: 60000,
color: true,
reporter: "spec",
},
// 路径配置
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts",
},
// TypeChain 配置
typechain: {
outDir: "typechain-types",
target: "ethers-v5",
alwaysGenerateOverloads: false,
externalArtifacts: ["external-artifacts/*.json"],
dontOverrideCompile: false,
},
// 合约大小分析
contractSizer: {
alphaSort: true,
disambiguatePaths: false,
runOnCompile: false,
strict: true,
only: [],
except: [],
},
// 文档生成配置
docgen: {
path: "./docs",
clear: true,
runOnCompile: false,
},
};
export default config;
2.3.2 TypeScript 配置
// tsconfig.json
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": ["es2020", "dom"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"types": ["node", "mocha", "chai"],
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@contracts/*": ["./contracts/*"],
"@test/*": ["./test/*"],
"@scripts/*": ["./scripts/*"],
"@typechain/*": ["./typechain-types/*"]
}
},
"include": [
"./scripts/**/*",
"./test/**/*",
"./typechain-types/**/*",
"./hardhat.config.ts",
"./tasks/**/*"
],
"exclude": [
"node_modules",
"dist",
"cache",
"artifacts",
"coverage"
],
"files": [
"./hardhat.config.ts"
]
}
2.3.3 ESLint 配置
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: false,
es2021: true,
mocha: true,
node: true,
},
plugins: ["@typescript-eslint"],
extends: [
"standard",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:node/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
project: "./tsconfig.json",
},
settings: {
node: {
tryExtensions: [".js", ".json", ".node", ".ts"],
},
},
rules: {
"node/no-unsupported-features/es-syntax": [
"error",
{ ignores: ["modules"] },
],
"node/no-missing-import": "off",
"node/no-unpublished-import": "off",
"node/no-unpublished-require": "off",
"node/no-unsupported-features/node-builtins": [
"error",
{ version: ">=16.0.0" },
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"no-console": ["warn", { allow: ["warn", "error", "info"] }],
"prettier/prettier": [
"error",
{
semi: true,
trailingComma: "es5",
singleQuote: false,
printWidth: 100,
tabWidth: 2,
useTabs: false,
},
],
},
overrides: [
{
files: ["hardhat.config.ts"],
rules: {
"node/no-unpublished-import": "off",
},
},
{
files: ["scripts/**/*.ts"],
rules: {
"no-process-exit": "off",
},
},
{
files: ["test/**/*.ts"],
rules: {
"no-unused-expressions": "off",
},
},
],
};
2.3.4 Prettier 配置
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
2.3.5 环境变量配置
# .env.example
# 复制此文件为 .env 并填入实际值
# ========== API Keys ==========
# Etherscan
ETHERSCAN_API_KEY=your_etherscan_api_key
# 其他区块链浏览器
POLYGONSCAN_API_KEY=your_polygonscan_api_key
ARBISCAN_API_KEY=your_arbiscan_api_key
OPTIMISM_API_KEY=your_optimism_api_key
# Gas 报告
COINMARKETCAP_API_KEY=your_coinmarketcap_api_key
# ========== RPC URLs ==========
# Ethereum
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key
GOERLI_RPC_URL=https://eth-goerli.g.alchemy.com/v2/your-api-key
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your-api-key
# Polygon
POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/your-api-key
MUMBAI_RPC_URL=https://polygon-mumbai.g.alchemy.com/v2/your-api-key
# Arbitrum
ARBITRUM_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/your-api-key
ARBITRUM_GOERLI_RPC_URL=https://arb-goerli.g.alchemy.com/v2/your-api-key
# Optimism
OPTIMISM_RPC_URL=https://opt-mainnet.g.alchemy.com/v2/your-api-key
OPTIMISM_GOERLI_RPC_URL=https://opt-goerli.g.alchemy.com/v2/your-api-key
# ========== 私钥配置 ==========
# 单个私钥
PRIVATE_KEY=your_private_key_here
# 多个私钥(用于多签或测试)
PRIVATE_KEY_1=first_private_key
PRIVATE_KEY_2=second_private_key
PRIVATE_KEY_3=third_private_key
# 助记词(可选)
MNEMONIC="test test test test test test test test test test test junk"
# ========== 其他配置 ==========
# Gas 报告
REPORT_GAS=true
# 分叉配置
FORKING_ENABLED=false
ETH_MAINNET_URL=${MAINNET_RPC_URL}
# IR 编译
VIA_IR=false
# 主网 Gas 价格(单位:gwei)
MAINNET_GAS_PRICE=30
第三章:智能合约开发
3.1 创建智能合约
3.1.1 基础合约示例
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
/**
* @title MyToken
* @dev 一个支持铸币、销毁和暂停功能的 ERC20 代币
*/
contract MyToken is ERC20, Ownable, Pausable {
using SafeMath for uint256;
// 最大供应量
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18; // 10亿代币
// 铸造权限
mapping(address => bool) public minters;
// 事件
event Mint(address indexed to, uint256 amount);
event Burn(address indexed from, uint256 amount);
event MinterAdded(address indexed account);
event MinterRemoved(address indexed account);
/**
* @dev 构造函数
* @param name 代币名称
* @param symbol 代币符号
* @param initialSupply 初始供应量
*/
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) {
require(initialSupply <= MAX_SUPPLY, "Initial supply exceeds max supply");
// 铸造初始供应给部署者
_mint(msg.sender, initialSupply);
// 部署者默认拥有铸造权限
minters[msg.sender] = true;
emit MinterAdded(msg.sender);
}
/**
* @dev 铸造新代币(仅限所有者或铸币者)
* @param to 接收地址
* @param amount 铸造数量
*/
function mint(address to, uint256 amount)
external
whenNotPaused
onlyMinter
{
require(totalSupply().add(amount) <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
emit Mint(to, amount);
}
/**
* @dev 销毁代币
* @param amount 销毁数量
*/
function burn(uint256 amount) external whenNotPaused {
_burn(msg.sender, amount);
emit Burn(msg.sender, amount);
}
/**
* @dev 批量转账
* @param recipients 接收者数组
* @param amounts 数量数组
*/
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external whenNotPaused {
require(recipients.length == amounts.length, "Arrays length mismatch");
for (uint256 i = 0; i < recipients.length; i++) {
transfer(recipients[i], amounts[i]);
}
}
/**
* @dev 添加铸币者(仅所有者)
* @param account 账户地址
*/
function addMinter(address account) external onlyOwner {
require(!minters[account], "Already a minter");
minters[account] = true;
emit MinterAdded(account);
}
/**
* @dev 移除铸币者(仅所有者)
* @param account 账户地址
*/
function removeMinter(address account) external onlyOwner {
require(minters[account], "Not a minter");
minters[account] = false;
emit MinterRemoved(account);
}
/**
* @dev 暂停所有转账(仅所有者)
*/
function pause() external onlyOwner {
_pause();
}
/**
* @dev 恢复所有转账(仅所有者)
*/
function unpause() external onlyOwner {
_unpause();
}
/**
* @dev 重写转账函数,添加暂停检查
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);
require(!paused(), "Token transfer while paused");
}
// 修改器
modifier onlyMinter() {
require(minters[msg.sender], "Caller is not a minter");
_;
}
}
3.1.2 接口定义
// contracts/interfaces/IMyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IMyToken {
// 事件
event Mint(address indexed to, uint256 amount);
event Burn(address indexed from, uint256 amount);
event MinterAdded(address indexed account);
event MinterRemoved(address indexed account);
// 只读函数
function MAX_SUPPLY() external view returns (uint256);
function minters(address account) external view returns (bool);
// 状态改变函数
function mint(address to, uint256 amount) external;
function burn(uint256 amount) external;
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external;
function addMinter(address account) external;
function removeMinter(address account) external;
function pause() external;
function unpause() external;
}
3.1.3 库合约
// contracts/libraries/TokenMath.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library TokenMath {
/**
* @dev 计算代币数量对应的百分比
* @param amount 代币数量
* @param percentage 百分比(乘以100,如50表示50%)
* @param totalSupply 总供应量
*/
function calculatePercentage(
uint256 amount,
uint256 percentage,
uint256 totalSupply
) internal pure returns (uint256) {
require(percentage <= 10000, "Percentage too high"); // 最多10000 = 100%
return (amount * percentage) / 10000;
}
/**
* @dev 检查地址是否为零地址
*/
function isZeroAddress(address addr) internal pure returns (bool) {
return addr == address(0);
}
/**
* @dev 安全地将 ETH 转账到指定地址
*/
function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, "ETH transfer failed");
}
}
3.2 编译合约
# 编译所有合约
npx hardhat compile
# 清除缓存并重新编译
npx hardhat clean
npx hardhat compile
# 查看编译详情
npx hardhat compile –verbose
# 只编译特定合约
npx hardhat compile –contracts contracts/MyToken.sol
# 查看合约大小
npx hardhat size-contracts
3.3 编译配置文件
// scripts/compile-config.json
{
"compilerOptions": {
"target": "0.8.19",
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"metadata": {
"bytecodeHash": "ipfs"
},
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": ["ast"]
}
},
"libraries": {}
},
"sources": {
"contracts/MyToken.sol": {
"content": "compiled from source"
}
}
}
第四章:测试开发
4.1 测试环境配置
4.1.1 测试配置文件
// test/test-config.ts
import { ethers } from "hardhat";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
export interface TestContext {
owner: SignerWithAddress;
user1: SignerWithAddress;
user2: SignerWithAddress;
user3: SignerWithAddress;
deployer: SignerWithAddress;
}
export async function setupTest(): Promise<TestContext> {
const [owner, user1, user2, user3, deployer] = await ethers.getSigners();
return {
owner,
user1,
user2,
user3,
deployer,
};
}
// 测试常量
export const TEST_CONSTANTS = {
TOKEN_NAME: "MyToken",
TOKEN_SYMBOL: "MTK",
INITIAL_SUPPLY: ethers.utils.parseEther("1000000"), // 100万
MINT_AMOUNT: ethers.utils.parseEther("1000"),
BURN_AMOUNT: ethers.utils.parseEther("100"),
MAX_SUPPLY: ethers.utils.parseEther("1000000000"), // 10亿
};
4.1.2 基础测试文件
// test/MyToken.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { MyToken, MyToken__factory } from "../typechain-types";
import { setupTest, TEST_CONSTANTS } from "./test-config";
describe("MyToken", function () {
// 测试上下文类型
type FixtureResult = {
token: MyToken;
owner: SignerWithAddress;
user1: SignerWithAddress;
user2: SignerWithAddress;
user3: SignerWithAddress;
};
// 测试夹具
async function deployTokenFixture(): Promise<FixtureResult> {
const { owner, user1, user2, user3 } = await setupTest();
const MyTokenFactory = (await ethers.getContractFactory(
"MyToken"
)) as MyToken__factory;
const token = await MyTokenFactory.deploy(
TEST_CONSTANTS.TOKEN_NAME,
TEST_CONSTANTS.TOKEN_SYMBOL,
TEST_CONSTANTS.INITIAL_SUPPLY
);
await token.deployed();
return { token, owner, user1, user2, user3 };
}
describe("部署", function () {
it("应该正确设置代币名称和符号", async function () {
const { token } = await loadFixture(deployTokenFixture);
expect(await token.name()).to.equal(TEST_CONSTANTS.TOKEN_NAME);
expect(await token.symbol()).to.equal(TEST_CONSTANTS.TOKEN_SYMBOL);
});
it("应该将初始供应量分配给部署者", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
expect(ownerBalance).to.equal(TEST_CONSTANTS.INITIAL_SUPPLY);
});
it("应该设置正确的总供应量", async function () {
const { token } = await loadFixture(deployTokenFixture);
const totalSupply = await token.totalSupply();
expect(totalSupply).to.equal(TEST_CONSTANTS.INITIAL_SUPPLY);
});
it("应该设置部署者为铸币者", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const isMinter = await token.minters(owner.address);
expect(isMinter).to.be.true;
});
});
describe("转账功能", function () {
it("应该允许账户之间转账", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
const transferAmount = ethers.utils.parseEther("100");
// 转账前余额
const ownerBalanceBefore = await token.balanceOf(owner.address);
const user1BalanceBefore = await token.balanceOf(user1.address);
// 执行转账
await token.connect(owner).transfer(user1.address, transferAmount);
// 验证转账后余额
const ownerBalanceAfter = await token.balanceOf(owner.address);
const user1BalanceAfter = await token.balanceOf(user1.address);
expect(ownerBalanceAfter).to.equal(
ownerBalanceBefore.sub(transferAmount)
);
expect(user1BalanceAfter).to.equal(
user1BalanceBefore.add(transferAmount)
);
});
it("应该触发 Transfer 事件", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
const transferAmount = ethers.utils.parseEther("100");
await expect(token.connect(owner).transfer(user1.address, transferAmount))
.to.emit(token, "Transfer")
.withArgs(owner.address, user1.address, transferAmount);
});
it("当余额不足时应该转账失败", async function () {
const { token, user1, user2 } = await loadFixture(deployTokenFixture);
const transferAmount = ethers.utils.parseEther("100");
// user1 没有代币,转账应该失败
await expect(
token.connect(user1).transfer(user2.address, transferAmount)
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
});
it("应该支持批量转账", async function () {
const { token, owner, user1, user2, user3 } = await loadFixture(
deployTokenFixture
);
const recipients = [user1.address, user2.address, user3.address];
const amounts = [
ethers.utils.parseEther("100"),
ethers.utils.parseEther("200"),
ethers.utils.parseEther("300"),
];
// 执行批量转账
await token.connect(owner).batchTransfer(recipients, amounts);
// 验证每个接收者的余额
expect(await token.balanceOf(user1.address)).to.equal(amounts[0]);
expect(await token.balanceOf(user2.address)).to.equal(amounts[1]);
expect(await token.balanceOf(user3.address)).to.equal(amounts[2]);
});
it("批量转账时数组长度不匹配应该失败", async function () {
const { token, owner, user1, user2 } = await loadFixture(
deployTokenFixture
);
const recipients = [user1.address, user2.address];
const amounts = [ethers.utils.parseEther("100")]; // 长度不匹配
await expect(
token.connect(owner).batchTransfer(recipients, amounts)
).to.be.revertedWith("Arrays length mismatch");
});
});
describe("铸币功能", function () {
it("铸币者应该可以铸造新代币", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
const mintAmount = TEST_CONSTANTS.MINT_AMOUNT;
const totalSupplyBefore = await token.totalSupply();
// 执行铸币
await token.connect(owner).mint(user1.address, mintAmount);
// 验证
const user1Balance = await token.balanceOf(user1.address);
const totalSupplyAfter = await token.totalSupply();
expect(user1Balance).to.equal(mintAmount);
expect(totalSupplyAfter).to.equal(totalSupplyBefore.add(mintAmount));
});
it("应该触发 Mint 事件", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
const mintAmount = TEST_CONSTANTS.MINT_AMOUNT;
await expect(token.connect(owner).mint(user1.address, mintAmount))
.to.emit(token, "Mint")
.withArgs(user1.address, mintAmount);
});
it("非铸币者不应该可以铸造代币", async function () {
const { token, user1, user2 } = await loadFixture(deployTokenFixture);
const mintAmount = TEST_CONSTANTS.MINT_AMOUNT;
await expect(
token.connect(user1).mint(user2.address, mintAmount)
).to.be.revertedWith("Caller is not a minter");
});
it("铸造超过最大供应量应该失败", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
// 尝试铸造超过最大供应量的代币
const excessiveAmount = TEST_CONSTANTS.MAX_SUPPLY.add(
ethers.utils.parseEther("1")
);
await expect(
token.connect(owner).mint(owner.address, excessiveAmount)
).to.be.revertedWith("Exceeds max supply");
});
});
describe("销毁功能", function () {
it("用户应该可以销毁自己的代币", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const burnAmount = TEST_CONSTANTS.BURN_AMOUNT;
const ownerBalanceBefore = await token.balanceOf(owner.address);
const totalSupplyBefore = await token.totalSupply();
// 执行销毁
await token.connect(owner).burn(burnAmount);
// 验证
const ownerBalanceAfter = await token.balanceOf(owner.address);
const totalSupplyAfter = await token.totalSupply();
expect(ownerBalanceAfter).to.equal(ownerBalanceBefore.sub(burnAmount));
expect(totalSupplyAfter).to.equal(totalSupplyBefore.sub(burnAmount));
});
it("应该触发 Burn 事件", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const burnAmount = TEST_CONSTANTS.BURN_AMOUNT;
await expect(token.connect(owner).burn(burnAmount))
.to.emit(token, "Burn")
.withArgs(owner.address, burnAmount);
});
});
describe("权限管理", function () {
it("所有者应该可以添加铸币者", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
// 验证 user1 初始不是铸币者
expect(await token.minters(user1.address)).to.be.false;
// 添加铸币者
await token.connect(owner).addMinter(user1.address);
// 验证
expect(await token.minters(user1.address)).to.be.true;
});
it("应该触发 MinterAdded 事件", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
await expect(token.connect(owner).addMinter(user1.address))
.to.emit(token, "MinterAdded")
.withArgs(user1.address);
});
it("非所有者不应该可以添加铸币者", async function () {
const { token, user1, user2 } = await loadFixture(deployTokenFixture);
await expect(
token.connect(user1).addMinter(user2.address)
).to.be.revertedWith("Ownable: caller is not the owner");
});
it("所有者应该可以移除铸币者", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
// 先添加为铸币者
await token.connect(owner).addMinter(user1.address);
expect(await token.minters(user1.address)).to.be.true;
// 移除铸币者
await token.connect(owner).removeMinter(user1.address);
// 验证
expect(await token.minters(user1.address)).to.be.false;
});
it("应该触发 MinterRemoved 事件", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
// 先添加
await token.connect(owner).addMinter(user1.address);
// 移除并验证事件
await expect(token.connect(owner).removeMinter(user1.address))
.to.emit(token, "MinterRemoved")
.withArgs(user1.address);
});
});
describe("暂停功能", function () {
it("所有者应该可以暂停合约", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
await token.connect(owner).pause();
expect(await token.paused()).to.be.true;
});
it("暂停时转账应该失败", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
// 暂停合约
await token.connect(owner).pause();
const transferAmount = ethers.utils.parseEther("100");
await expect(
token.connect(owner).transfer(user1.address, transferAmount)
).to.be.revertedWith("Token transfer while paused");
});
it("暂停时铸造应该失败", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
// 暂停合约
await token.connect(owner).pause();
const mintAmount = TEST_CONSTANTS.MINT_AMOUNT;
await expect(
token.connect(owner).mint(user1.address, mintAmount)
).to.be.revertedWith("Pausable: paused");
});
it("所有者应该可以恢复合约", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
// 暂停
await token.connect(owner).pause();
expect(await token.paused()).to.be.true;
// 恢复
await token.connect(owner).unpause();
expect(await token.paused()).to.be.false;
});
it("非所有者不应该可以暂停合约", async function () {
const { token, user1 } = await loadFixture(deployTokenFixture);
await expect(token.connect(user1).pause()).to.be.revertedWith(
"Ownable: caller is not the owner"
);
});
});
describe("边界情况和安全测试", function () {
it("不应该允许向零地址转账", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const transferAmount = ethers.utils.parseEther("100");
await expect(
token.connect(owner).transfer(ethers.constants.AddressZero, transferAmount)
).to.be.revertedWith("ERC20: transfer to the zero address");
});
it("不应该允许铸造到零地址", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const mintAmount = TEST_CONSTANTS.MINT_AMOUNT;
await expect(
token.connect(owner).mint(ethers.constants.AddressZero, mintAmount)
).to.be.revertedWith("ERC20: mint to the zero address");
});
it("应该正确处理大额数字", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const largeAmount = TEST_CONSTANTS.MAX_SUPPLY.sub(
TEST_CONSTANTS.INITIAL_SUPPLY
);
// 铸造剩余的最大供应量
await token.connect(owner).mint(owner.address, largeAmount);
// 验证总供应量等于最大供应量
const totalSupply = await token.totalSupply();
expect(totalSupply).to.equal(TEST_CONSTANTS.MAX_SUPPLY);
});
it("Gas 消耗测试", async function () {
const { token, owner, user1 } = await loadFixture(deployTokenFixture);
const transferAmount = ethers.utils.parseEther("100");
// 测量转账的 Gas 消耗
const tx = await token.connect(owner).transfer(user1.address, transferAmount);
const receipt = await tx.wait();
console.log(`转账 Gas 消耗: ${receipt.gasUsed.toString()}`);
// 验证 Gas 消耗在合理范围内
expect(receipt.gasUsed.toNumber()).to.be.lessThan(100000);
});
});
});
4.1.3 集成测试
// test/integration/TokenFlow.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { MyToken, MyToken__factory } from "../../typechain-types";
describe("MyToken 集成测试", function () {
async function deployFixture() {
const [owner, user1, user2, user3] = await ethers.getSigners();
const MyTokenFactory = (await ethers.getContractFactory(
"MyToken"
)) as MyToken__factory;
const token = await MyTokenFactory.deploy(
"Integration Token",
"INT",
ethers.utils.parseEther("1000000")
);
await token.deployed();
return { token, owner, user1, user2, user3 };
}
describe("完整业务流程", function () {
it("应该完成完整的代币生命周期", async function () {
const { token, owner, user1, user2 } = await loadFixture(deployFixture);
// 1. 初始状态验证
const initialOwnerBalance = await token.balanceOf(owner.address);
expect(initialOwnerBalance).to.equal(ethers.utils.parseEther("1000000"));
// 2. 转账
const transferAmount = ethers.utils.parseEther("500");
await token.connect(owner).transfer(user1.address, transferAmount);
expect(await token.balanceOf(user1.address)).to.equal(transferAmount);
// 3. 添加铸币者
await token.connect(owner).addMinter(user1.address);
expect(await token.minters(user1.address)).to.be.true;
// 4. 新铸币者铸造代币
const mintAmount = ethers.utils.parseEther("200");
await token.connect(user1).mint(user2.address, mintAmount);
expect(await token.balanceOf(user2.address)).to.equal(mintAmount);
// 5. 批量转账
const recipients = [owner.address, user1.address];
const amounts = [
ethers.utils.parseEther("50"),
ethers.utils.parseEther("30"),
];
await token.connect(user2).batchTransfer(recipients, amounts);
// 6. 销毁代币
const burnAmount = ethers.utils.parseEther("100");
await token.connect(owner).burn(burnAmount);
// 7. 暂停合约
await token.connect(owner).pause();
expect(await token.paused()).to.be.true;
// 8. 验证暂停后不能转账
await expect(
token.connect(user1).transfer(owner.address, ethers.utils.parseEther("10"))
).to.be.revertedWith("Token transfer while paused");
// 9. 恢复合约
await token.connect(owner).unpause();
expect(await token.paused()).to.be.false;
// 10. 最终余额验证
const finalTotalSupply = await token.totalSupply();
console.log(`最终总供应量: ${ethers.utils.formatEther(finalTotalSupply)}`);
// 验证所有操作后的总供应量
const expectedTotalSupply = ethers.utils.parseEther("1000000")
.add(mintAmount) // 铸造增加
.sub(burnAmount); // 销毁减少
expect(finalTotalSupply).to.equal(expectedTotalSupply);
});
});
});
4.2 运行测试
# 运行所有测试
npx hardhat test
# 运行特定测试文件
npx hardhat test ./test/MyToken.test.ts
# 运行特定测试套件
npx hardhat test –grep "部署"
# 并行运行测试(加快速度)
npx hardhat test –parallel
# 显示详细日志
npx hardhat test –verbose
# 生成测试覆盖率报告
npx hardhat coverage
# 运行测试并生成 Gas 报告
REPORT_GAS=true npx hardhat test
# 在指定网络运行测试
npx hardhat test –network hardhat
# 运行集成测试
npx hardhat test ./test/integration/
# 调试测试(在 VS Code 中)
# 1. 点击测试旁边的调试按钮
# 2. 或使用 F5 启动调试
4.3 测试覆盖率
# 安装覆盖率工具
npm install –save-dev solidity-coverage
# 在 hardhat.config.ts 中配置
import "solidity-coverage";
# 运行覆盖率测试
npx hardhat coverage
# 生成 HTML 报告
npx hardhat coverage –testfiles "test/*.ts" –solcoverjs .solcover.js
第五章:部署脚本
5.1 基础部署脚本
// scripts/deploy.ts
import { ethers, upgrades, network, run } from "hardhat";
import fs from "fs";
import path from "path";
// 部署配置接口
interface DeploymentConfig {
tokenName: string;
tokenSymbol: string;
initialSupply: string;
verifyContract: boolean;
saveDeployment: boolean;
}
// 默认部署配置
const DEFAULT_CONFIG: DeploymentConfig = {
tokenName: "MyToken",
tokenSymbol: "MTK",
initialSupply: "1000000", // 100万代币
verifyContract: true,
saveDeployment: true,
};
async function main() {
console.log(`🚀 开始部署到网络: ${network.name}`);
// 获取部署配置
const config = await getDeploymentConfig();
console.log("📋 部署配置:", config);
// 获取部署者账户
const [deployer] = await ethers.getSigners();
console.log(`👤 部署者地址: ${deployer.address}`);
console.log(`💰 部署者余额: ${ethers.utils.formatEther(await deployer.getBalance())} ETH`);
// 部署合约
console.log("⏳ 正在部署 MyToken 合约…");
const MyToken = await ethers.getContractFactory("MyToken");
const initialSupply = ethers.utils.parseEther(config.initialSupply);
const token = await MyToken.deploy(
config.tokenName,
config.tokenSymbol,
initialSupply
);
await token.deployed();
console.log(`✅ MyToken 合约部署成功!`);
console.log(`📝 合约地址: ${token.address}`);
console.log(`🏷️ 代币名称: ${await token.name()}`);
console.log(`🔤 代币符号: ${await token.symbol()}`);
console.log(`📊 总供应量: ${ethers.utils.formatEther(await token.totalSupply())}`);
// 验证合约(如果需要)
if (config.verifyContract && network.name !== "hardhat" && network.name !== "localhost") {
console.log("🔍 正在验证合约…");
await verifyContract(token.address, [
config.tokenName,
config.tokenSymbol,
initialSupply,
]);
}
// 保存部署信息(如果需要)
if (config.saveDeployment) {
await saveDeploymentInfo(token, config);
}
// 执行部署后操作
await postDeploymentActions(token, deployer);
console.log("🎉 部署流程完成!");
}
/**
* 获取部署配置
*/
async function getDeploymentConfig(): Promise<DeploymentConfig> {
// 可以根据网络调整配置
const networkName = network.name;
let config = { …DEFAULT_CONFIG };
// 网络特定配置
switch (networkName) {
case "mainnet":
config.verifyContract = true;
config.initialSupply = "10000000"; // 主网使用1000万
break;
case "goerli":
case "sepolia":
config.verifyContract = true;
config.initialSupply = "1000000"; // 测试网使用100万
break;
case "hardhat":
case "localhost":
config.verifyContract = false;
break;
}
// 从环境变量覆盖配置
if (process.env.TOKEN_NAME) {
config.tokenName = process.env.TOKEN_NAME;
}
if (process.env.TOKEN_SYMBOL) {
config.tokenSymbol = process.env.TOKEN_SYMBOL;
}
if (process.env.INITIAL_SUPPLY) {
config.initialSupply = process.env.INITIAL_SUPPLY;
}
if (process.env.VERIFY_CONTRACT) {
config.verifyContract = process.env.VERIFY_CONTRACT === "true";
}
return config;
}
/**
* 验证合约
*/
async function verifyContract(contractAddress: string, args: any[]) {
try {
console.log(`⏳ 等待区块确认…`);
// 等待足够的区块确认
await new Promise(resolve => setTimeout(resolve, 30000));
console.log(`🔧 开始验证合约 ${contractAddress}…`);
await run("verify:verify", {
address: contractAddress,
constructorArguments: args,
});
console.log("✅ 合约验证成功!");
} catch (error: any) {
if (error.message.toLowerCase().includes("already verified")) {
console.log("📝 合约已经验证过了");
} else {
console.error("❌ 合约验证失败:", error);
}
}
}
/**
* 保存部署信息
*/
async function saveDeploymentInfo(token: any, config: DeploymentConfig) {
const networkName = network.name;
const chainId = network.config.chainId;
const deploymentDir = path.join(__dirname, "..", "deployments");
const networkDir = path.join(deploymentDir, networkName);
// 创建目录
if (!fs.existsSync(networkDir)) {
fs.mkdirSync(networkDir, { recursive: true });
}
// 部署信息
const deploymentInfo = {
network: networkName,
chainId: chainId,
contract: "MyToken",
address: token.address,
deployer: (await ethers.getSigners())[0].address,
deploymentTime: new Date().toISOString(),
config: config,
transactionHash: token.deployTransaction.hash,
blockNumber: token.deployTransaction.blockNumber || "pending",
abi: JSON.parse(token.interface.format(ethers.utils.FormatTypes.json) as string),
};
// 保存到文件
const deploymentFile = path.join(networkDir, "MyToken.json");
fs.writeFileSync(
deploymentFile,
JSON.stringify(deploymentInfo, null, 2)
);
console.log(`💾 部署信息已保存到: ${deploymentFile}`);
// 同时保存到前端目录(如果存在)
const frontendDir = path.join(__dirname, "..", "frontend", "src", "contracts");
if (fs.existsSync(frontendDir)) {
const frontendInfo = {
address: token.address,
abi: deploymentInfo.abi,
network: networkName,
chainId: chainId,
};
fs.writeFileSync(
path.join(frontendDir, "MyToken.json"),
JSON.stringify(frontendInfo, null, 2)
);
console.log("📱 合约信息已保存到前端目录");
}
}
/**
* 部署后操作
*/
async function postDeploymentActions(token: any, deployer: any) {
console.log("⚙️ 执行部署后操作…");
try {
// 示例:添加额外的铸币者
const additionalMinter = process.env.ADDITIONAL_MINTER;
if (additionalMinter) {
console.log(`👥 添加额外铸币者: ${additionalMinter}`);
const tx = await token.connect(deployer).addMinter(additionalMinter);
await tx.wait();
console.log("✅ 额外铸币者添加成功");
}
// 示例:铸造额外代币
const extraMintAmount = process.env.EXTRA_MINT_AMOUNT;
if (extraMintAmount) {
console.log(`🪙 铸造额外代币: ${extraMintAmount}`);
const amount = ethers.utils.parseEther(extraMintAmount);
const tx = await token.connect(deployer).mint(deployer.address, amount);
await tx.wait();
console.log("✅ 额外代币铸造成功");
}
// 示例:转移所有权
const newOwner = process.env.NEW_OWNER;
if (newOwner) {
console.log(`👑 转移合约所有权到: ${newOwner}`);
const tx = await token.connect(deployer).transferOwnership(newOwner);
await tx.wait();
console.log("✅ 所有权转移成功");
}
} catch (error) {
console.error("❌ 部署后操作失败:", error);
}
}
/**
* 错误处理
*/
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("💥 部署失败:", error);
process.exit(1);
});
5.2 多网络部署脚本
// scripts/deploy-multiple.ts
import { ethers, network, run } from "hardhat";
import { DeployFunction } from "hardhat-deploy/types";
import { HardhatRuntimeEnvironment } from "hardhat/types";
// 部署函数
const deployFunc: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployments, getNamedAccounts } = hre;
const { deploy, log } = deployments;
const { deployer } = await getNamedAccounts();
log(`🚀 部署 MyToken 到 ${network.name} 网络…`);
// 网络特定配置
const networkConfig = getNetworkConfig(network.name);
const MyToken = await deploy("MyToken", {
from: deployer,
args: [
networkConfig.tokenName,
networkConfig.tokenSymbol,
ethers.utils.parseEther(networkConfig.initialSupply),
],
log: true,
waitConfirmations: networkConfig.waitConfirmations,
});
log(`✅ MyToken 部署成功: ${MyToken.address}`);
// 验证合约
if (networkConfig.verify && MyToken.transactionHash) {
log("🔍 验证合约…");
await verifyContract(MyToken.address, [
networkConfig.tokenName,
networkConfig.tokenSymbol,
ethers.utils.parseEther(networkConfig.initialSupply),
]);
}
return true;
};
// 网络配置
function getNetworkConfig(networkName: string) {
const defaultConfig = {
tokenName: "MyToken",
tokenSymbol: "MTK",
initialSupply: "1000000",
waitConfirmations: 1,
verify: false,
};
const configs: Record<string, any> = {
hardhat: {
…defaultConfig,
verify: false,
},
localhost: {
…defaultConfig,
verify: false,
},
goerli: {
…defaultConfig,
initialSupply: "500000",
waitConfirmations: 6,
verify: true,
},
sepolia: {
…defaultConfig,
initialSupply: "500000",
waitConfirmations: 6,
verify: true,
},
mainnet: {
…defaultConfig,
tokenName: "MyToken Mainnet",
initialSupply: "10000000",
waitConfirmations: 12,
verify: true,
},
polygon: {
…defaultConfig,
tokenName: "MyToken Polygon",
initialSupply: "1000000",
waitConfirmations: 5,
verify: true,
},
};
return configs[networkName] || defaultConfig;
}
// 验证合约
async function verifyContract(contractAddress: string, args: any[]) {
try {
await run("verify:verify", {
address: contractAddress,
constructorArguments: args,
});
} catch (error: any) {
if (error.message.toLowerCase().includes("already verified")) {
console.log("✅ 合约已经验证过了");
} else {
console.error("❌ 验证失败:", error);
}
}
}
export default deployFunc;
deployFunc.tags = ["MyToken"];
5.3 升级部署脚本
// scripts/deploy-upgradeable.ts
import { ethers, upgrades, network } from "hardhat";
async function main() {
console.log(`🚀 部署可升级合约到 ${network.name}`);
const [deployer] = await ethers.getSigners();
console.log(`👤 部署者: ${deployer.address}`);
// 部署逻辑合约
console.log("⏳ 部署 MyTokenV1…");
const MyTokenV1 = await ethers.getContractFactory("MyTokenV1");
const proxy = await upgrades.deployProxy(MyTokenV1, [
"MyToken",
"MTK",
ethers.utils.parseEther("1000000"),
], {
initializer: "initialize",
kind: "uups", // 或 "transparent"
});
await proxy.deployed();
console.log(`✅ 代理合约地址: ${proxy.address}`);
// 获取逻辑合约地址
const logicAddress = await upgrades.erc1967.getImplementationAddress(proxy.address);
console.log(`🔧 逻辑合约地址: ${logicAddress}`);
// 获取代理管理员地址
const adminAddress = await upgrades.erc1967.getAdminAddress(proxy.address);
console.log(`👑 代理管理员地址: ${adminAddress}`);
// 保存部署信息
const deploymentInfo = {
network: network.name,
chainId: network.config.chainId,
proxyAddress: proxy.address,
logicAddress: logicAddress,
adminAddress: adminAddress,
deployer: deployer.address,
deploymentTime: new Date().toISOString(),
};
console.log("📋 部署信息:", deploymentInfo);
// 验证逻辑合约
if (network.name !== "hardhat" && network.name !== "localhost") {
console.log("🔍 验证逻辑合约…");
try {
await run("verify:verify", {
address: logicAddress,
});
} catch (error) {
console.error("验证失败:", error);
}
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
5.4 运行部署脚本
# 部署到本地网络
npx hardhat run scripts/deploy.ts –network localhost
# 部署到测试网
npx hardhat run scripts/deploy.ts –network goerli
# 部署到主网
npx hardhat run scripts/deploy.ts –network mainnet
# 带参数部署
TOKEN_NAME="MyAwesomeToken" TOKEN_SYMBOL="MAT" npx hardhat run scripts/deploy.ts –network goerli
# 部署并验证
npx hardhat run scripts/deploy.ts –network sepolia –verify
# 部署可升级合约
npx hardhat run scripts/deploy-upgradeable.ts –network goerli
# 使用 hardhat-deploy 插件
npx hardhat deploy –network goerli –tags MyToken
# 交互式部署(使用 console)
npx hardhat console –network localhost
> const MyToken = await ethers.getContractFactory("MyToken")
> const token = await MyToken.deploy("Test", "TST", ethers.utils.parseEther("1000"))
> await token.deployed()
> console.log("合约地址:", token.address)
第六章:交互与调试
6.1 Hardhat Console 使用
# 启动 Hardhat Console
npx hardhat console –network localhost
# 在 console 中操作
> const [owner, user1] = await ethers.getSigners()
> const MyToken = await ethers.getContractFactory("MyToken")
> const token = await MyToken.deploy("MyToken", "MTK", ethers.utils.parseEther("1000000"))
> await token.deployed()
> console.log("合约地址:", token.address)
# 调用合约方法
> await token.name()
'MyToken'
> await token.balanceOf(owner.address)
BigNumber { _hex: '0x…', _isBigNumber: true }
> await token.transfer(user1.address, ethers.utils.parseEther("100"))
> await token.balanceOf(user1.address)
BigNumber { _hex: '0x…', _isBigNumber: true }
# 查看交易
> const tx = await token.transfer(user1.address, ethers.utils.parseEther("50"))
> await tx.wait()
> console.log("交易哈希:", tx.hash)
# 退出 console
> .exit
6.2 调试交易
// scripts/debug-transaction.ts
import { ethers } from "hardhat";
async function debugTransaction() {
// 1. 获取合约实例
const tokenAddress = "0x…"; // 替换为实际地址
const MyToken = await ethers.getContractFactory("MyToken");
const token = MyToken.attach(tokenAddress);
// 2. 获取最近的事件
console.log("📜 获取最近的事件…");
const filter = token.filters.Transfer();
const events = await token.queryFilter(filter, -10000, 'latest');
console.log("最近转账事件:", events.length);
// 3. 调试特定交易
const txHash = "0x…"; // 替换为交易哈希
console.log(`🔍 调试交易: ${txHash}`);
const tx = await ethers.provider.getTransaction(txHash);
console.log("交易详情:", {
from: tx.from,
to: tx.to,
value: ethers.utils.formatEther(tx.value),
gasPrice: ethers.utils.formatUnits(tx.gasPrice!, "gwei"),
data: tx.data,
});
const receipt = await ethers.provider.getTransactionReceipt(txHash);
console.log("交易收据:", {
status: receipt.status,
gasUsed: receipt.gasUsed.toString(),
logs: receipt.logs.length,
});
// 4. 使用 Hardhat 调试器
console.log("🐛 使用 Hardhat 调试器…");
// 在终端运行: npx hardhat debug <txHash>
// 5. 分析 Gas 消耗
console.log("⛽ 分析 Gas 消耗…");
const gasUsed = receipt.gasUsed;
const gasPrice = tx.gasPrice!;
const gasCost = gasUsed.mul(gasPrice);
console.log(`Gas 消耗: ${gasUsed.toString()}`);
console.log(`Gas 成本: ${ethers.utils.formatEther(gasCost)} ETH`);
}
debugTransaction()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
6.3 交互脚本
// scripts/interact.ts
import { ethers } from "hardhat";
async function interactWithContract() {
console.log("🤖 开始与合约交互…");
// 1. 获取合约实例
const tokenAddress = process.env.TOKEN_ADDRESS;
if (!tokenAddress) {
throw new Error("请设置 TOKEN_ADDRESS 环境变量");
}
const MyToken = await ethers.getContractFactory("MyToken");
const token = MyToken.attach(tokenAddress);
// 2. 获取基本信息
console.log("📊 获取合约基本信息…");
const name = await token.name();
const symbol = await token.symbol();
const totalSupply = await token.totalSupply();
const owner = await token.owner();
console.log(`代币名称: ${name}`);
console.log(`代币符号: ${symbol}`);
console.log(`总供应量: ${ethers.utils.formatEther(totalSupply)}`);
console.log(`所有者: ${owner}`);
// 3. 获取账户余额
const [deployer, user1] = await ethers.getSigners();
const deployerBalance = await token.balanceOf(deployer.address);
const user1Balance = await token.balanceOf(user1.address);
console.log(`\\n💰 账户余额:`);
console.log(`部署者: ${ethers.utils.formatEther(deployerBalance)} ${symbol}`);
console.log(`用户1: ${ethers.utils.formatEther(user1Balance)} ${symbol}`);
// 4. 执行转账
console.log(`\\n🔄 执行转账…`);
const transferAmount = ethers.utils.parseEther("100");
console.log(`从部署者转账 100 ${symbol} 到用户1…`);
const tx = await token.connect(deployer).transfer(user1.address, transferAmount);
await tx.wait();
console.log("✅ 转账成功!");
// 5. 验证转账结果
const newDeployerBalance = await token.balanceOf(deployer.address);
const newUser1Balance = await token.balanceOf(user1.address);
console.log(`\\n📊 转账后余额:`);
console.log(`部署者: ${ethers.utils.formatEther(newDeployerBalance)} ${symbol}`);
console.log(`用户1: ${ethers.utils.formatEther(newUser1Balance)} ${symbol}`);
// 6. 检查铸币者权限
console.log(`\\n👥 检查铸币者权限…`);
const isDeployerMinter = await token.minters(deployer.address);
const isUser1Minter = await token.minters(user1.address);
console.log(`部署者是铸币者: ${isDeployerMinter}`);
console.log(`用户1是铸币者: ${isUser1Minter}`);
// 7. 如果部署者是铸币者,执行铸币
if (isDeployerMinter) {
console.log(`\\n🪙 执行铸币操作…`);
const mintAmount = ethers.utils.parseEther("500");
console.log(`铸币 500 ${symbol} 给用户1…`);
const mintTx = await token.connect(deployer).mint(user1.address, mintAmount);
await mintTx.wait();
console.log("✅ 铸币成功!");
const finalUser1Balance = await token.balanceOf(user1.address);
console.log(`用户1最终余额: ${ethers.utils.formatEther(finalUser1Balance)} ${symbol}`);
}
// 8. 获取合约事件
console.log(`\\n📜 获取最近的事件…`);
const transferEvents = await token.queryFilter(
token.filters.Transfer(),
-1000,
'latest'
);
console.log(`最近 ${transferEvents.length} 次转账事件:`);
transferEvents.slice(-5).forEach((event, i) => {
console.log(` ${i + 1}. From: ${event.args!.from} -> To: ${event.args!.to}, Amount: ${ethers.utils.formatEther(event.args!.value)}`);
});
}
interactWithContract()
.then(() => {
console.log("\\n🎉 交互完成!");
process.exit(0);
})
.catch((error) => {
console.error("❌ 交互失败:", error);
process.exit(1);
});
第七章:自动化与工作流
7.1 npm 脚本配置
// package.json 中的 scripts 部分
{
"scripts": {
// 开发
"compile": "hardhat compile",
"clean": "hardhat clean",
"typechain": "hardhat typechain",
// 测试
"test": "hardhat test",
"test:watch": "hardhat test –watch",
"test:gas": "REPORT_GAS=true hardhat test",
"test:coverage": "hardhat coverage",
"test:debug": "hardhat test –debug",
// 代码质量
"lint": "npm run lint:sol && npm run lint:ts",
"lint:sol": "solhint 'contracts/**/*.sol'",
"lint:ts": "eslint '**/*.ts' –fix",
"format": "prettier –write '**/*.{js,ts,json,md,sol}'",
"format:check": "prettier –check '**/*.{js,ts,json,md,sol}'",
// 安全分析
"slither": "slither .",
"mythril": "myth analyze contracts/*.sol",
// 部署
"deploy:local": "hardhat run scripts/deploy.ts –network localhost",
"deploy:goerli": "hardhat run scripts/deploy.ts –network goerli",
"deploy:sepolia": "hardhat run scripts/deploy.ts –network sepolia",
"deploy:mainnet": "hardhat run scripts/deploy.ts –network mainnet",
// 验证
"verify:goerli": "hardhat verify –network goerli",
"verify:sepolia": "hardhat verify –network sepolia",
// 节点
"node": "hardhat node",
"node:fork": "FORKING_ENABLED=true hardhat node",
// 控制台
"console": "hardhat console –network localhost",
"console:goerli": "hardhat console –network goerli",
// 任务
"accounts": "hardhat accounts",
"balance": "hardhat balance –address",
"flatten": "hardhat flatten",
// 完整工作流
"build": "npm run clean && npm run compile && npm run typechain",
"validate": "npm run lint && npm run test && npm run build",
"predeploy": "npm run validate",
"ci": "npm run lint && npm run test:coverage && npm run build"
}
}
7.2 Git Hooks 配置
// package.json 中的 husky 配置
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm run test",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.sol": [
"solhint –fix",
"prettier –write"
],
"*.{js,ts}": [
"eslint –fix",
"prettier –write"
],
"*.{json,md}": [
"prettier –write"
]
}
}
# 安装 Git Hooks 工具
npm install –save-dev husky lint-staged @commitlint/cli @commitlint/config-conventional
# 初始化 husky
npx husky install
# 添加 pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"
# 添加 commit-msg hook
npx husky add .husky/commit-msg 'npx –no-install commitlint –edit "$1"'
// commitlint.config.js
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feat", // 新功能
"fix", // 修复bug
"docs", // 文档更新
"style", // 代码格式调整
"refactor", // 代码重构
"test", // 测试相关
"chore", // 构建过程或辅助工具变动
"revert", // 回滚提交
"perf", // 性能优化
"ci", // CI配置
"build" // 构建系统
],
],
},
};
7.3 CI/CD 配置
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
steps:
– uses: actions/checkout@v3
– name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
– name: Install dependencies
run: npm ci
– name: Lint
run: npm run lint
– name: Run tests
run: npm test
– name: Run coverage
run: npm run test:coverage
– name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
– name: Build project
run: npm run build
deploy-staging:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v3
– name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
– name: Install dependencies
run: npm ci
– name: Deploy to Sepolia Testnet
run: |
npx hardhat run scripts/deploy.ts –network sepolia
env:
SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
deploy-production:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v3
– name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
– name: Install dependencies
run: npm ci
– name: Deploy to Mainnet
run: |
npx hardhat run scripts/deploy.ts –network mainnet
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
第八章:高级功能与优化
8.1 自定义 Hardhat 任务
// tasks/index.ts
import { task } from "hardhat/config";
import { HardhatRuntimeEnvironment } from "hardhat/types";
// 账户相关任务
task("accounts", "显示所有账户和余额")
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const accounts = await hre.ethers.getSigners();
console.log("📋 账户列表:");
console.log("=".repeat(80));
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i];
const balance = await account.getBalance();
const ethBalance = hre.ethers.utils.formatEther(balance);
console.log(`#${i}: ${account.address}`);
console.log(` 余额: ${ethBalance} ETH`);
console.log(` 交易次数: ${await account.getTransactionCount()}`);
console.log("-".repeat(80));
}
});
// 余额查询任务
task("balance", "查询指定地址的余额")
.addParam("address", "要查询的地址")
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const balance = await hre.ethers.provider.getBalance(taskArgs.address);
const ethBalance = hre.ethers.utils.formatEther(balance);
console.log(`💰 地址 ${taskArgs.address} 的余额:`);
console.log(` ${ethBalance} ETH`);
console.log(` ${balance.toString()} Wei`);
});
// 网络信息任务
task("network", "显示当前网络信息")
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const network = hre.network;
const provider = hre.ethers.provider;
console.log("🌐 网络信息:");
console.log("=".repeat(80));
console.log(`名称: ${network.name}`);
console.log(`链ID: ${network.config.chainId}`);
const blockNumber = await provider.getBlockNumber();
console.log(`当前区块: ${blockNumber}`);
const gasPrice = await provider.getGasPrice();
console.log(`当前Gas价格: ${hre.ethers.utils.formatUnits(gasPrice, "gwei")} Gwei`);
const feeData = await provider.getFeeData();
console.log(`基础费用: ${hre.ethers.utils.formatUnits(feeData.maxFeePerGas!, "gwei")} Gwei`);
console.log(`优先费用: ${hre.ethers.utils.formatUnits(feeData.maxPriorityFeePerGas!, "gwei")} Gwei`);
});
// 合约验证任务
task("verify-contract", "验证合约")
.addParam("address", "合约地址")
.addOptionalVariadicPositionalParam("args", "构造函数参数", [])
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
console.log(`🔍 验证合约 ${taskArgs.address}…`);
try {
await hre.run("verify:verify", {
address: taskArgs.address,
constructorArguments: taskArgs.args,
});
console.log("✅ 合约验证成功!");
} catch (error: any) {
if (error.message.toLowerCase().includes("already verified")) {
console.log("📝 合约已经验证过了");
} else {
console.error("❌ 验证失败:", error.message);
}
}
});
// Gas 估算任务
task("estimate-gas", "估算合约方法调用的Gas消耗")
.addParam("contract", "合约名称")
.addParam("method", "方法名称")
.addOptionalVariadicPositionalParam("args", "方法参数", [])
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const { contract, method, args } = taskArgs;
console.log(`⛽ 估算 ${contract}.${method} 的Gas消耗…`);
const Contract = await hre.ethers.getContractFactory(contract);
const contractInstance = await Contract.deploy();
await contractInstance.deployed();
try {
const estimate = await contractInstance.estimateGas[method](…args);
console.log(`预估Gas消耗: ${estimate.toString()}`);
const gasPrice = await hre.ethers.provider.getGasPrice();
const gasCost = estimate.mul(gasPrice);
console.log(`预估成本: ${hre.ethers.utils.formatEther(gasCost)} ETH`);
} catch (error: any) {
console.error("❌ 估算失败:", error.message);
}
});
// 导出任务
export {};
8.2 安全分析配置
// scripts/security-analysis.ts
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
async function runSecurityAnalysis() {
console.log("🛡️ 开始安全分析…");
try {
// 1. 运行 Slither
console.log("\\n1. 运行 Slither 分析…");
try {
const { stdout: slitherOutput } = await execAsync("slither .");
console.log("Slither 分析完成");
console.log(slitherOutput);
} catch (slitherError: any) {
console.log("Slither 输出:", slitherError.stdout);
}
// 2. 运行 Mythril
console.log("\\n2. 运行 Mythril 分析…");
try {
const { stdout: mythrilOutput } = await execAsync(
"myth analyze contracts/MyToken.sol"
);
console.log("Mythril 分析完成");
console.log(mythrilOutput);
} catch (mythrilError: any) {
console.log("Mythril 输出:", mythrilError.stdout);
}
// 3. 运行 Solhint
console.log("\\n3. 运行 Solhint 检查…");
try {
const { stdout: solhintOutput } = await execAsync("npm run lint:sol");
console.log("Solhint 检查完成");
console.log(solhintOutput);
} catch (solhintError: any) {
console.log("Solhint 发现问题:", solhintError.stdout);
}
// 4. 运行 Echidna(如果安装)
console.log("\\n4. 运行 Echidna 模糊测试…");
try {
const { stdout: echidnaOutput } = await execAsync(
"echidna-test contracts/MyToken.sol –contract MyToken"
);
console.log("Echidna 测试完成");
console.log(echidnaOutput);
} catch (echidnaError: any) {
console.log("Echidna 测试失败或未安装:", echidnaError.message);
}
console.log("\\n✅ 安全分析完成!");
} catch (error) {
console.error("❌ 安全分析失败:", error);
}
}
// 安装安全分析工具
async function installSecurityTools() {
console.log("🔧 安装安全分析工具…");
const tools = [
{ name: "Slither", command: "pip3 install slither-analyzer" },
{ name: "Mythril", command: "pip3 install mythril" },
{ name: "Echidna", command: "brew install echidna" }, // macOS
{ name: "Solhint", command: "npm install -g solhint" },
];
for (const tool of tools) {
console.log(`安装 ${tool.name}…`);
try {
await execAsync(tool.command);
console.log(`✅ ${tool.name} 安装成功`);
} catch (error) {
console.log(`⚠️ ${tool.name} 安装失败:`, error);
}
}
}
// 运行分析
runSecurityAnalysis();
8.3 性能优化
8.3.1 编译优化
// hardhat.config.ts 中的优化配置
const config: HardhatUserConfig = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 1000, // 根据合约使用频率调整
details: {
yul: true,
yulDetails: {
stackAllocation: true,
optimizerSteps: "dhfoDgvulfnTUtnIf"
}
}
},
viaIR: true, // 启用 IR 编译(减少字节码大小)
metadata: {
bytecodeHash: "ipfs", // 使用 IPFS 哈希
},
outputSelection: {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": ["ast"]
}
}
},
},
};
8.3.2 测试优化
// 使用 fixture 提高测试性能
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
// 在多个测试中共享部署状态
async function deployFixture() {
const [owner, user1, user2] = await ethers.getSigners();
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy(
"Test Token",
"TEST",
ethers.utils.parseEther("1000000")
);
return { token, owner, user1, user2 };
}
describe("MyToken 测试", function () {
// 使用 fixture,避免每次测试都重新部署
it("测试1", async function () {
const { token, owner } = await loadFixture(deployFixture);
// 测试逻辑
});
it("测试2", async function () {
const { token, owner, user1 } = await loadFixture(deployFixture);
// 测试逻辑
});
});
第九章:故障排除与调试
9.1 常见问题解决
9.1.1 编译错误
# 1. 清除缓存重新编译
npx hardhat clean
npx hardhat compile
# 2. 检查 Solidity 版本兼容性
# 确保 hardhat.config.ts 和合约中的版本一致
# 3. 检查导入路径
# 确保 OpenZeppelin 等依赖已正确安装
# 4. 查看详细错误信息
npx hardhat compile –verbose
9.1.2 测试失败
# 1. 增加测试超时时间
# 在 hardhat.config.ts 中配置 mocha.timeout
# 2. 查看详细错误堆栈
npx hardhat test –verbose
# 3. 单独运行失败测试
npx hardhat test –grep "特定测试名"
# 4. 调试测试
npx hardhat test –debug
9.1.3 部署失败
# 1. 检查网络连接
# 确保 RPC URL 正确且可访问
# 2. 检查账户余额
# 确保部署账户有足够的 ETH 支付 Gas
# 3. 检查 Gas 价格
# 在网络拥堵时适当提高 Gas 价格
# 4. 查看部署日志
npx hardhat run scripts/deploy.ts –network goerli –verbose
9.1.4 验证失败
# 1. 等待足够区块确认
# 某些网络需要等待多个区块确认后才能验证
# 2. 检查构造函数参数
# 确保验证时使用的参数与部署时一致
# 3. 使用 flatten 命令
# 如果合约有多个文件,先 flatten
npx hardhat flatten contracts/MyToken.sol > MyTokenFlattened.sol
# 4. 手动验证
# 在 Etherscan 上手动验证
9.2 调试技巧
9.2.1 使用 console.log
// 在 Solidity 0.8.0+ 中使用 console.log
pragma solidity ^0.8.19;
import "hardhat/console.sol"; // 导入 Hardhat 的 console
contract DebugContract {
uint256 public value;
function setValue(uint256 _value) public {
console.log("设置前的值:", value);
console.log("新值:", _value);
console.log("调用者:", msg.sender);
value = _value;
console.log("设置后的值:", value);
}
}
9.2.2 使用 Hardhat Network 日志
// 在 hardhat.config.ts 中启用详细日志
const config: HardhatUserConfig = {
networks: {
hardhat: {
loggingEnabled: true, // 启用日志
chainId: 31337,
},
},
};
// 或在测试中启用
beforeEach(async function () {
// 启用 Hardhat Network 日志
await network.provider.send("hardhat_setLoggingEnabled", [true]);
});
9.2.3 交易追踪
// 追踪交易执行
async function traceTransaction(txHash: string) {
const trace = await network.provider.send("debug_traceTransaction", [
txHash,
{
tracer: "callTracer",
tracerConfig: {
onlyTopCall: false,
withLog: true,
},
},
]);
console.log("交易追踪:", JSON.stringify(trace, null, 2));
}
// 或在 Hardhat Console 中
> const tx = await token.transfer(user1.address, 100)
> await tx.wait()
> console.log(await network.provider.send("debug_traceTransaction", [tx.hash]))
第十章:最佳实践总结
10.1 项目结构最佳实践
project/
├── contracts/ # 智能合约
│ ├── interfaces/ # 接口定义
│ ├── libraries/ # 库合约
│ ├── tokens/ # 代币相关合约
│ ├── governance/ # 治理合约
│ ├── staking/ # 质押合约
│ └── utils/ # 工具合约
├── scripts/ # 脚本
│ ├── deploy/ # 部署脚本
│ ├── upgrade/ # 升级脚本
│ ├── tasks/ # Hardhat 任务
│ └── utils/ # 脚本工具函数
├── test/ # 测试
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ ├── fixtures/ # 测试夹具
│ └── mocks/ # 模拟合约
├── deployments/ # 部署记录
├── docs/ # 文档
├── .vscode/ # VS Code 配置
├── .github/ # GitHub 工作流
└── config/ # 配置文件
10.2 代码质量最佳实践
10.2.1 Solidity 编码规范
// 1. 使用明确的 SPDX 许可证标识
// SPDX-License-Identifier: MIT
// 2. 使用稳定的 Solidity 版本
pragma solidity ^0.8.19;
// 3. 导入顺序:标准库 -> 第三方库 -> 本地合约
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../interfaces/IMyToken.sol";
import "../libraries/TokenMath.sol";
// 4. 合约结构顺序:
// – 类型声明
// – 状态变量
// – 事件
// – 修饰器
// – 构造函数
// – 接收/回退函数
// – 外部函数
// – 公开函数
// – 内部函数
// – 私有函数
// 5. 使用 NatSpec 注释
/**
* @title MyToken
* @author Your Name
* @notice 这是一个示例 ERC20 代币合约
* @dev 实现了铸币、销毁和暂停功能
*/
contract MyToken is ERC20, Ownable {
// 状态变量注释
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18;
// 事件注释
event Mint(address indexed to, uint256 amount);
/**
* @dev 构造函数
* @param name 代币名称
* @param symbol 代币符号
* @param initialSupply 初始供应量
*/
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) {
// 构造函数逻辑
}
/**
* @dev 铸造新代币
* @param to 接收地址
* @param amount 铸造数量
* @notice 仅所有者或铸币者可调用
* @custom:requirements 铸造后总供应量不能超过 MAX_SUPPLY
*/
function mint(address to, uint256 amount) external onlyOwner {
// 函数逻辑
}
}
10.2.2 安全最佳实践
// 1. 使用 SafeMath(Solidity 0.8+ 已内置)
// 2. 检查-效果-交互模式
function withdraw(uint256 amount) external nonReentrant {
// 检查
require(balances[msg.sender] >= amount, "余额不足");
require(amount > 0, "金额必须大于0");
// 效果
balances[msg.sender] -= amount;
totalSupply -= amount;
// 交互
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "转账失败");
}
// 3. 使用 OpenZeppelin 的安全合约
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
// 4. 避免硬编码值
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18;
address public constant TREASURY = 0x…;
// 5. 适当的访问控制
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier onlyMinter() {
require(minters[msg.sender], "Not minter");
_;
}
// 6. 输入验证
function transfer(address to, uint256 amount) public override returns (bool) {
require(to != address(0), "Transfer to zero address");
require(amount > 0, "Transfer amount must be positive");
require(balanceOf(msg.sender) >= amount, "Insufficient balance");
return super.transfer(to, amount);
}
10.3 部署最佳实践
10.3.1 多阶段部署
// scripts/deploy-phased.ts
async function phasedDeployment() {
console.log("🚀 开始多阶段部署");
// 阶段1:部署核心合约
console.log("\\n📦 阶段1: 部署核心合约");
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy(…);
await token.deployed();
console.log(`✅ MyToken 部署: ${token.address}`);
// 等待确认
await new Promise(resolve => setTimeout(resolve, 30000));
// 阶段2:配置合约
console.log("\\n⚙️ 阶段2: 配置合约");
await token.addMinter(treasuryAddress);
await token.transfer(treasuryAddress, initialTreasuryAmount);
console.log("✅ 合约配置完成");
// 阶段3:验证合约
console.log("\\n🔍 阶段3: 验证合约");
await run("verify:verify", {
address: token.address,
constructorArguments: […],
});
console.log("✅ 合约验证完成");
// 阶段4:转移所有权(如果需要)
console.log("\\n👑 阶段4: 转移所有权");
await token.transferOwnership(multisigAddress);
console.log(`✅ 所有权转移到: ${multisigAddress}`);
console.log("\\n🎉 多阶段部署完成!");
}
10.3.2 紧急情况处理
// scripts/emergency.ts
// 紧急情况处理脚本
async function handleEmergency() {
const tokenAddress = process.env.TOKEN_ADDRESS;
const emergencyKey = process.env.EMERGENCY_PRIVATE_KEY;
if (!tokenAddress || !emergencyKey) {
throw new Error("缺少必要的环境变量");
}
// 连接到合约
const emergencySigner = new ethers.Wallet(emergencyKey, ethers.provider);
const MyToken = await ethers.getContractFactory("MyToken", emergencySigner);
const token = MyToken.attach(tokenAddress);
console.log("🚨 执行紧急操作");
// 1. 暂停合约
console.log("⏸️ 暂停合约…");
const pauseTx = await token.pause();
await pauseTx.wait();
console.log("✅ 合约已暂停");
// 2. 转移剩余资金到安全地址
console.log("💰 转移资金到安全地址…");
const safeAddress = process.env.SAFE_ADDRESS;
const balance = await token.balanceOf(tokenAddress);
if (balance.gt(0)) {
const transferTx = await token.transfer(safeAddress, balance);
await transferTx.wait();
console.log(`✅ 已转移 ${ethers.utils.formatEther(balance)} 代币到安全地址`);
}
// 3. 记录紧急操作
console.log("📝 记录紧急操作…");
const emergencyInfo = {
timestamp: new Date().toISOString(),
action: "emergency_pause",
performer: emergencySigner.address,
tokenAddress: tokenAddress,
safeAddress: safeAddress,
transferredAmount: balance.toString(),
};
console.log("紧急操作记录:", emergencyInfo);
console.log("🛡️ 紧急操作完成");
}
10.4 监控与维护
10.4.1 事件监听
// scripts/monitor.ts
import { ethers } from "hardhat";
async function monitorContract() {
const tokenAddress = process.env.TOKEN_ADDRESS;
const MyToken = await ethers.getContractFactory("MyToken");
const token = MyToken.attach(tokenAddress);
console.log("👂 开始监听合约事件…");
// 监听所有 Transfer 事件
token.on("Transfer", (from, to, value, event) => {
console.log(`🔄 转账事件:`);
console.log(` 从: ${from}`);
console.log(` 到: ${to}`);
console.log(` 金额: ${ethers.utils.formatEther(value)}`);
console.log(` 交易哈希: ${event.transactionHash}`);
console.log(` 区块号: ${event.blockNumber}`);
console.log("-".repeat(50));
});
// 监听 Mint 事件
token.on("Mint", (to, amount, event) => {
console.log(`🪙 铸币事件:`);
console.log(` 接收者: ${to}`);
console.log(` 金额: ${ethers.utils.formatEther(amount)}`);
console.log("-".repeat(50));
});
// 监听暂停事件
token.on("Paused", (account) => {
console.log(`⏸️ 合约被暂停`);
console.log(` 操作者: ${account}`);
console.log("-".repeat(50));
});
// 监听恢复事件
token.on("Unpaused", (account) => {
console.log(`▶️ 合约恢复`);
console.log(` 操作者: ${account}`);
console.log("-".repeat(50));
});
// 保持脚本运行
console.log("监控中…按 Ctrl+C 停止");
process.on("SIGINT", () => {
console.log("\\n🛑 停止监控");
token.removeAllListeners();
process.exit(0);
});
}
monitorContract().catch(console.error);
10.4.2 定期检查脚本
// scripts/health-check.ts
import { ethers } from "hardhat";
async function healthCheck() {
console.log("🏥 执行合约健康检查");
const tokenAddress = process.env.TOKEN_ADDRESS;
const MyToken = await ethers.getContractFactory("MyToken");
const token = MyToken.attach(tokenAddress);
const checks = [
{
name: "合约状态",
check: async () => {
const paused = await token.paused();
return !paused;
},
errorMessage: "合约处于暂停状态",
},
{
name: "总供应量",
check: async () => {
const totalSupply = await token.totalSupply();
const maxSupply = await token.MAX_SUPPLY();
return totalSupply.lte(maxSupply);
},
errorMessage: "总供应量超过最大供应量",
},
{
name: "所有者权限",
check: async () => {
const owner = await token.owner();
return owner !== ethers.constants.AddressZero;
},
errorMessage: "所有者地址为零地址",
},
{
name: "代币符号",
check: async () => {
const symbol = await token.symbol();
return symbol.length > 0;
},
errorMessage: "代币符号为空",
},
];
let allPassed = true;
for (const check of checks) {
try {
const passed = await check.check();
if (passed) {
console.log(`✅ ${check.name}: 正常`);
} else {
console.log(`❌ ${check.name}: ${check.errorMessage}`);
allPassed = false;
}
} catch (error) {
console.log(`⚠️ ${check.name}: 检查失败 – ${error.message}`);
allPassed = false;
}
}
if (allPassed) {
console.log("\\n🎉 所有健康检查通过!");
} else {
console.log("\\n⚠️ 发现潜在问题,请及时处理");
process.exit(1);
}
}
healthCheck().catch(console.error);
总结
本指南详细介绍了如何使用 VS Code 和 Hardhat 进行智能合约开发的全流程,包括:
环境搭建:配置 VS Code 和安装必要扩展
项目初始化:创建和配置 Hardhat TypeScript 项目
智能合约开发:编写、编译和优化 Solidity 合约
测试开发:编写全面的单元测试和集成测试
部署流程:多网络部署、验证和升级
交互调试:使用 Hardhat Console 和调试工具
自动化工作流:配置 CI/CD 和 Git Hooks
高级功能:自定义任务、安全分析和性能优化
故障排除:常见问题解决和调试技巧
最佳实践:代码质量、安全和部署的最佳实践
通过遵循本指南,您可以建立一个专业、高效且可维护的智能合约开发环境,提高开发效率并确保代码质量。记住,智能合约开发需要特别注意安全性,务必进行充分测试和安全审计后再部署到生产环境。


