引言
Web3 开发正在重塑互联网的底层架构,为开发者提供了构建去中心化应用(DApps)的全新范式。本文将深入探讨如何使用现代工具链(Ethers.js + Hardhat)进行智能合约开发、测试与部署的全过程,涵盖从基础概念到高级实践的各个细节。
第一部分:环境配置与工具链搭建
1.1 开发环境准备
# 初始化项目
mkdir web3-dapp && cd web3-dapp
npm init -y
# 安装核心依赖
npm install --save-dev hardhat ethers @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers
# 创建Hardhat项目
npx hardhat
选择"Create a basic sample project"并确认所有选项,这将生成以下目录结构:
contracts/ - Solidity智能合约源文件
scripts/ - 部署脚本
test/ - 测试文件
hardhat.config.js - Hardhat配置文件
1.2 配置多网络支持
修改hardhat.config.js以支持多网络部署:
require('@nomiclabs/hardhat-waffle');
const INFURA_PROJECT_ID = 'your-infura-project-id';
const PRIVATE_KEY = 'your-metamask-private-key';
module.exports = {
solidity: "0.8.4",
networks: {
hardhat: {
chainId: 1337
},
ropsten: {
url: `https://ropsten.infura.io/v3/${INFURA_PROJECT_ID}`,
accounts: [PRIVATE_KEY]
},
mainnet: {
url: `https://mainnet.infura.io/v3/${INFURA_PROJECT_ID}`,
accounts: [PRIVATE_KEY]
}
}
};
第二部分:智能合约开发详解
2.1 编写ERC20代币合约
创建contracts/MyToken.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
uint8 private _decimals;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_,
uint256 initialSupply_
) ERC20(name_, symbol_) {
_decimals = decimals_;
_mint(msg.sender, initialSupply_ * 10**decimals_);
}
function decimals() public view override returns (uint8) {
return _decimals;
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}
关键点解析:
- 使用OpenZeppelin标准库确保安全性
- 自定义小数位数处理
- 实现铸币(mint)和销毁(burn)功能
- 采用Ownable模式进行权限控制
2.2 编写NFT合约(ERC721)
创建contracts/MyNFT.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFT is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721("MyNFT", "MNFT") {}
function mintNFT(address recipient, string memory tokenURI)
public
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
第三部分:测试策略与实现
3.1 编写ERC20测试用例
创建test/MyToken.test.js:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken Contract", function () {
let Token;
let hardhatToken;
let owner;
let addr1;
let addr2;
let addrs;
beforeEach(async function () {
Token = await ethers.getContractFactory("MyToken");
[owner, addr1, addr2, ...addrs] = await ethers.getSigners();
hardhatToken = await Token.deploy(
"My Token",
"MTK",
18,
1000000
);
});
describe("Deployment", function () {
it("Should assign the total supply of tokens to the owner", async function () {
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
it("Should set the correct decimals", async function () {
expect(await hardhatToken.decimals()).to.equal(18);
});
});
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
await hardhatToken.transfer(addr1.address, 100);
const addr1Balance = await hardhatToken.balanceOf(addr1.address);
expect(addr1Balance).to.equal(100);
await hardhatToken.connect(addr1).transfer(addr2.address, 50);
const addr2Balance = await hardhatToken.balanceOf(addr2.address);
expect(addr2Balance).to.equal(50);
});
it("Should fail if sender doesn't have enough tokens", async function () {
const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);
await expect(
hardhatToken.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
expect(await hardhatToken.balanceOf(owner.address)).to.equal(
initialOwnerBalance
);
});
});
describe("Minting", function () {
it("Should mint new tokens", async function () {
const initialSupply = await hardhatToken.totalSupply();
await hardhatToken.mint(addr1.address, 1000);
expect(await hardhatToken.totalSupply()).to.equal(initialSupply.add(1000));
});
it("Should reject minting from non-owner", async function () {
await expect(
hardhatToken.connect(addr1).mint(addr1.address, 1000)
).to.be.revertedWith("Ownable: caller is not the owner");
});
});
});
3.2 高级测试技巧
- 使用
hardhat-network-helpers模拟时间流逝:
const helpers = require("@nomicfoundation/hardhat-network-helpers");
// 快进1小时
await helpers.time.increase(3600);
// 快进到特定时间戳
await helpers.time.increaseTo(1640995200);
- Gas消耗测试:
const tx = await hardhatToken.transfer(addr1.address, 100);
const receipt = await tx.wait();
console.log("Gas used:", receipt.gasUsed.toString());
第四部分:部署脚本与前端集成
4.1 编写自动化部署脚本
创建scripts/deploy.js:
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
console.log("Account balance:", (await deployer.getBalance()).toString());
// 部署ERC20代币
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy(
"Web3 Token",
"WEB3",
18,
1000000
);
await token.deployed();
console.log("Token address:", token.address);
// 部署NFT合约
const NFT = await ethers.getContractFactory("MyNFT");
const nft = await NFT.deploy();
await nft.deployed();
console.log("NFT address:", nft.address);
// 保存部署地址到前端可以访问的文件
const fs = require("fs");
const contractsDir = __dirname + "/../frontend/src/contracts";
if (!fs.existsSync(contractsDir)) {
fs.mkdirSync(contractsDir);
}
fs.writeFileSync(
contractsDir + "/contract-addresses.json",
JSON.stringify({
Token: token.address,
NFT: nft.address
}, undefined, 2)
);
// 保存ABI
const tokenArtifact = artifacts.readArtifactSync("MyToken");
const nftArtifact = artifacts.readArtifactSync("MyNFT");
fs.writeFileSync(
contractsDir + "/Token.json",
JSON.stringify(tokenArtifact, null, 2)
);
fs.writeFileSync(
contractsDir + "/NFT.json",
JSON.stringify(nftArtifact, null, 2)
);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
4.2 前端集成(Ethers.js + React)
安装前端依赖:
npm install ethers @web3-react/core @web3-react/injected-connector
创建React连接器:
// src/connectors.js
import { InjectedConnector } from '@web3-react/injected-connector';
export const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42, 1337]
});
创建自定义hook:
// src/hooks/useWeb3.js
import { useWeb3React } from '@web3-react/core';
import { injected } from '../connectors';
import { useEffect, useState } from 'react';
import TokenABI from '../contracts/Token.json';
import NFTABI from '../contracts/NFT.json';
export function useWeb3() {
const { active, account, library, connector, activate, deactivate } = useWeb3React();
const [tokenContract, setTokenContract] = useState(null);
const [nftContract, setNFTContract] = useState(null);
const [balance, setBalance] = useState('0');
useEffect(() => {
const init = async () => {
try {
await activate(injected);
} catch (err) {
console.error(err);
}
};
if (!active) {
init();
}
}, [active, activate]);
useEffect(() => {
if (account && library) {
// 初始化合约实例
const token = new library.eth.Contract(
TokenABI.abi,
TokenABI.networks[library._network.chainId]?.address
);
const nft = new library.eth.Contract(
NFTABI.abi,
NFTABI.networks[library._network.chainId]?.address
);
setTokenContract(token);
setNFTContract(nft);
// 获取余额
token.methods.balanceOf(account).call()
.then(bal => setBalance(bal.toString()));
}
}, [account, library]);
const disconnect = () => {
deactivate();
setTokenContract(null);
setNFTContract(null);
};
const mintToken = async (amount) => {
return tokenContract.methods.mint(account, amount)
.send({ from: account });
};
const mintNFT = async (tokenURI) => {
return nftContract.methods.mintNFT(account, tokenURI)
.send({ from: account });
};
return {
active,
account,
balance,
tokenContract,
nftContract,
disconnect,
mintToken,
mintNFT
};
}
第五部分:高级开发技巧
5.1 Gas优化策略
- 批量操作模式:
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external {
require(recipients.length == amounts.length, "Arrays length mismatch");
for (uint i = 0; i < recipients.length; i++) {
_transfer(msg.sender, recipients[i], amounts[i]);
}
}
- 使用EIP-712结构化签名:
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(deadline >= block.timestamp, "Expired permit");
bytes32 structHash = keccak256(
abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
nonces[owner]++,
deadline
)
);
bytes32 hash = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
structHash
)
);
address signer = ecrecover(hash, v, r, s);
require(signer == owner, "Invalid signature");
_approve(owner, spender, value);
}
5.2 安全最佳实践
- 重入攻击防护:
// 使用Checks-Effects-Interactions模式
function withdraw() external nonReentrant {
uint amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0; // 先更新状态
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
- 签名验证:
function verify(
address signer,
bytes32 hash,
bytes memory signature
) internal pure returns (bool) {
require(signature.length == 65, "Invalid signature length");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
if (v < 27) v += 27;
require(v == 27 || v == 28, "Invalid signature version");
return signer == ecrecover(hash, v, r, s);
}
结语
Web3开发是一个快速发展的领域,本指南涵盖了从智能合约开发到前端集成的完整流程。随着生态系统的成熟,开发者需要持续关注EIP提案、Layer2解决方案和新的开发工具。建议定期查阅OpenZeppelin文档、Hardhat更新和以太坊官方博客,以保持技术栈的更新。
通过本文介绍的技术栈和最佳实践,您已经具备了构建安全、高效的Web3应用程序的基础能力。下一步可以探索更复杂的DeFi协议开发、DAO治理机制或跨链互操作性解决方案。