通过智能合约进阶开发与实战项目,掌握复杂合约架构设计、资金流管理、权限控制与多人交互机制,实战构建投票、众筹等Web3核心应用,提升区块链开发能力。
作者
经验值
阅读时间
下面是从零开始创建一个 Hardhat 项目并部署到 Sepolia 测试网的全流程,包括每一步命令、代码文件内容和注意事项。非常适合第一次操作 Hardhat 的开发者。
mkdir counter-hardhat
cd counter-hardhat
npm init -y
npm install --save-dev hardhat
npx hardhat
选择:
> Create a basic sample project
按提示一路回车。Hardhat 会创建以下结构:
counter-hardhat/
├── contracts/
│ └── Lock.sol <- 示例合约
├── scripts/
│ └── deploy.js <- 部署脚本
├── test/
│ └── Lock.js <- 示例测试
├── hardhat.config.js <- Hardhat 配置文件
删除 contracts/Lock.sol,新建 contracts/Counter.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Counter {
uint256 private count;
constructor(uint256 _init) {
count = _init;
}
function increment() public {
count += 1;
}
function decrement() public {
require(count > 0, "Counter is already zero");
count -= 1;
}
function getCount() public view returns (uint256) {
return count;
}
}
新建 test/counter-test.js:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Counter 合约测试", function () {
let counter;
beforeEach(async function () {
const Counter = await ethers.getContractFactory("Counter");
counter = await Counter.deploy(5);
await counter.deployed();
});
it("初始值为 5", async function () {
expect(await counter.getCount()).to.equal(5);
});
it("increment 应加 1", async function () {
await counter.increment();
expect(await counter.getCount()).to.equal(6);
});
it("decrement 应减 1", async function () {
await counter.decrement();
expect(await counter.getCount()).to.equal(4);
});
});
运行测试:
npx hardhat test
修改 scripts/deploy.js 为:
const hre = require("hardhat");
async function main() {
const Counter = await hre.ethers.getContractFactory("Counter");
const counter = await Counter.deploy(10); // 初始值
await counter.deployed();
console.log("Counter 部署成功,地址为:", counter.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
npm install dotenv
touch .env
.env 内容如下:
PRIVATE_KEY=0x你的钱包私钥(建议使用测试钱包)
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/你的infura项目ID
注意:请勿提交 .env 到 git,务必保密!
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY],
},
},
};
先确保你有 Sepolia 测试币。
然后执行部署命令:
npx hardhat run scripts/deploy.js --network sepolia
输出示例:
Counter 部署成功,地址为:0xA1b2C3...789
你可以在 Sepolia Etherscan 查看这个地址的合约。
新建 scripts/interact.js:
const hre = require("hardhat");
async function main() {
const address = "替换为你部署的合约地址";
const Counter = await hre.ethers.getContractFactory("Counter");
const counter = Counter.attach(address);
await counter.increment();
const current = await counter.getCount();
console.log("当前计数值是:", current.toString());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
执行交互脚本:
npx hardhat run scripts/interact.js --network sepolia
counter-hardhat/
├── contracts/
│ └── Counter.sol
├── scripts/
│ ├── deploy.js
│ └── interact.js
├── test/
│ └── counter-test.js
├── .env
├── hardhat.config.js
├── package.json
└── node_modules/
创建一个 ERC20 代币你有两种方式可以选择:
这种方式代码安全、简单、可维护,适合绝大多数场景。
步骤如下:
npm install @openzeppelin/contracts
下面是一个带有**交易手续费(比如每笔收 1% 到 owner)**的 ERC20 代币合约:
// 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 {
uint256 public feePercent = 1; // 1% 手续费
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000000 * 10 ** decimals()); // 初始发行100万个
}
function setFeePercent(uint256 _fee) external onlyOwner {
require(_fee <= 10, "fee too high");
feePercent = _fee;
}
// 重写 transfer 和 transferFrom 来加手续费
function _transfer(address sender, address recipient, uint256 amount) internal override {
uint256 fee = (amount * feePercent) / 100;
uint256 amountAfterFee = amount - fee;
super._transfer(sender, owner(), fee); // 收手续费到 owner
super._transfer(sender, recipient, amountAfterFee);
}
}
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken 合约", function () {
let MyToken, token, owner, addr1, addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
MyToken = await ethers.getContractFactory("MyToken");
token = await MyToken.deploy();
await token.deployed();
});
it("初始供应量应该分配给合约部署者", async function () {
const ownerBalance = await token.balanceOf(owner.address);
const totalSupply = await token.totalSupply();
expect(ownerBalance).to.equal(totalSupply);
});
it("用户之间转账应该扣除手续费", async function () {
const amount = ethers.utils.parseUnits("100", 18); // 100 MTK
const fee = amount.mul(1).div(100); // 1% 手续费 = 1 MTK
const received = amount.sub(fee);
// 给 addr1 一些代币
await token.transfer(addr1.address, amount);
// addr1 转账给 addr2
await token.connect(addr1).transfer(addr2.address, amount);
const addr2Balance = await token.balanceOf(addr2.address);
expect(addr2Balance).to.equal(received);
const ownerBalance = await token.balanceOf(owner.address);
expect(ownerBalance).to.be.gt(0); // owner 收到手续费
});
it("只有 owner 能修改手续费比例", async function () {
await token.setFeePercent(5);
expect(await token.feePercent()).to.equal(5);
await expect(
token.connect(addr1).setFeePercent(3)
).to.be.revertedWith("Ownable: caller is not the owner");
});
it("手续费比例不能超过 10%", async function () {
await expect(token.setFeePercent(11)).to.be.revertedWith("fee too high");
});
});
// scripts/deploy-token.js
const hre = require("hardhat");
async function main() {
const Token = await hre.ethers.getContractFactory("MyToken");
const token = await Token.deploy();
await token.deployed();
console.log("Token 部署地址:", token.address);
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
部署:
npx hardhat run scripts/deploy-token.js --network sepolia
如果你想锻炼底层原理,也可以手动写 ERC20 标准合约(不推荐正式生产使用):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyERC20 {
string public name = "MyToken";
string public symbol = "MTK";
uint8 public decimals = 18;
uint256 public totalSupply;
address public owner;
uint256 public feePercent = 1;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
constructor() {
owner = msg.sender;
_mint(msg.sender, 1000000 * 10 ** decimals);
}
function _mint(address to, uint256 amount) internal {
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
function transfer(address to, uint256 amount) public returns (bool) {
uint256 fee = (amount * feePercent) / 100;
uint256 amountAfterFee = amount - fee;
balanceOf[msg.sender] -= amount;
balanceOf[to] += amountAfterFee;
balanceOf[owner] += fee;
emit Transfer(msg.sender, to, amountAfterFee);
emit Transfer(msg.sender, owner, fee);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(allowance[from][msg.sender] >= amount, "Not allowed");
allowance[from][msg.sender] -= amount;
uint256 fee = (amount * feePercent) / 100;
uint256 amountAfterFee = amount - fee;
balanceOf[from] -= amount;
balanceOf[to] += amountAfterFee;
balanceOf[owner] += fee;
emit Transfer(from, to, amountAfterFee);
emit Transfer(from, owner, fee);
return true;
}
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
方式 | 是否推荐 | 特点 |
OpenZeppelin | 推荐 | 安全、维护方便,支持多种扩展(如权限控制、升级) |
手写 ERC20 | 不推荐 | 适合学习和展示,不建议在真实网络部署 |
这里为你展示一个多人参与的简单“众筹 + 投票”智能合约示例,特点如下:
mapping
管理捐款记录、提案、投票情况。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CrowdFund {
address public owner;
uint256 public totalFund;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
struct Proposal {
string description;
address payable recipient;
uint256 amount;
uint256 yesVotes;
uint256 noVotes;
bool executed;
mapping(address => bool) voted;
}
mapping(address => uint256) public contributions;
mapping(uint256 => Proposal) public proposals;
uint256 public numProposals;
event Contributed(address indexed from, uint256 amount);
event ProposalCreated(uint256 id, string description);
event Voted(uint256 id, address voter, bool vote);
event ProposalExecuted(uint256 id);
// ----------------------------
// 1. 捐款
// ----------------------------
function contribute() external payable {
require(msg.value > 0, "Must send ETH");
contributions[msg.sender] += msg.value;
totalFund += msg.value;
emit Contributed(msg.sender, msg.value);
}
// ----------------------------
// 2. 创建提案
// ----------------------------
function createProposal(string memory _desc, address payable _to, uint256 _amount) external onlyOwner {
require(_amount <= totalFund, "Insufficient fund");
Proposal storage p = proposals[numProposals];
p.description = _desc;
p.recipient = _to;
p.amount = _amount;
emit ProposalCreated(numProposals, _desc);
numProposals++;
}
// ----------------------------
// 3. 投票
// ----------------------------
function vote(uint256 proposalId, bool support) external {
require(contributions[msg.sender] > 0, "Not a contributor");
Proposal storage p = proposals[proposalId];
require(!p.voted[msg.sender], "Already voted");
require(!p.executed, "Already executed");
p.voted[msg.sender] = true;
if (support) {
p.yesVotes++;
} else {
p.noVotes++;
}
emit Voted(proposalId, msg.sender, support);
}
// ----------------------------
// 4. 执行提案
// ----------------------------
function executeProposal(uint256 proposalId) external onlyOwner {
Proposal storage p = proposals[proposalId];
require(!p.executed, "Already executed");
uint256 totalVoters = getVoterCount();
require(p.yesVotes > totalVoters / 2, "Not enough yes votes");
p.executed = true;
totalFund -= p.amount;
p.recipient.transfer(p.amount);
emit ProposalExecuted(proposalId);
}
// 辅助函数:获取参与投票人数
function getVoterCount() public view returns (uint256 count) {
for (uint256 i = 0; i < numProposals; i++) {
Proposal storage p = proposals[i];
count += p.yesVotes + p.noVotes;
}
}
}
功能 | 数据结构 | 说明 |
捐款记录 | mapping(address => uint256) | 保存每个地址的捐款 |
提案 | mapping(uint256 => Proposal) | 通过编号管理所有提案 |
投票记录 | Proposal.voted[address] | 防止重复投票 |
执行资金 | Proposal.recipient.transfer(...) | 通过表决后,转账给指定地址 |
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("CrowdFund 合约测试", function () {
let CrowdFund, contract, owner, addr1, addr2, addr3;
beforeEach(async function () {
[owner, addr1, addr2, addr3] = await ethers.getSigners();
const CrowdFundFactory = await ethers.getContractFactory("CrowdFund");
contract = await CrowdFundFactory.deploy();
await contract.deployed();
});
it("应记录捐款并更新总额", async function () {
await contract.connect(addr1).contribute({ value: ethers.utils.parseEther("1") });
await contract.connect(addr2).contribute({ value: ethers.utils.parseEther("2") });
const c1 = await contract.contributions(addr1.address);
const c2 = await contract.contributions(addr2.address);
const total = await contract.totalFund();
expect(c1).to.equal(ethers.utils.parseEther("1"));
expect(c2).to.equal(ethers.utils.parseEther("2"));
expect(total).to.equal(ethers.utils.parseEther("3"));
});
it("仅 owner 可以创建提案", async function () {
await expect(
contract.connect(addr1).createProposal("测试提案", addr3.address, ethers.utils.parseEther("1"))
).to.be.revertedWith("Only owner");
await expect(
contract.createProposal("测试提案", addr3.address, ethers.utils.parseEther("1"))
).to.emit(contract, "ProposalCreated");
});
it("捐款人可以对提案投票", async function () {
await contract.connect(addr1).contribute({ value: ethers.utils.parseEther("1") });
await contract.createProposal("提案", addr2.address, ethers.utils.parseEther("0.5"));
await expect(contract.connect(addr1).vote(0, true))
.to.emit(contract, "Voted")
.withArgs(0, addr1.address, true);
});
it("禁止重复投票", async function () {
await contract.connect(addr1).contribute({ value: ethers.utils.parseEther("1") });
await contract.createProposal("提案", addr2.address, ethers.utils.parseEther("0.5"));
await contract.connect(addr1).vote(0, true);
await expect(contract.connect(addr1).vote(0, false)).to.be.revertedWith("Already voted");
});
it("多数通过后 owner 可执行提案并支付", async function () {
// 贡献资金
await contract.connect(addr1).contribute({ value: ethers.utils.parseEther("1") });
await contract.connect(addr2).contribute({ value: ethers.utils.parseEther("1") });
// 创建提案
await contract.createProposal("发钱给 addr3", addr3.address, ethers.utils.parseEther("1"));
// 投票通过
await contract.connect(addr1).vote(0, true);
await contract.connect(addr2).vote(0, true);
// 执行提案
await expect(contract.executeProposal(0))
.to.emit(contract, "ProposalExecuted");
const balance = await ethers.provider.getBalance(addr3.address);
expect(balance).to.be.gt(ethers.utils.parseEther("9999")); // 初始余额 ~10000 ETH
});
it("若未过半数支持,则不能执行提案", async function () {
await contract.connect(addr1).contribute({ value: ethers.utils.parseEther("1") });
await contract.connect(addr2).contribute({ value: ethers.utils.parseEther("1") });
await contract.createProposal("不给钱", addr3.address, ethers.utils.parseEther("1"));
await contract.connect(addr1).vote(0, false);
await expect(contract.executeProposal(0)).to.be.revertedWith("Not enough yes votes");
});
});
npx hardhat test
这个测试覆盖了:
测试点 | 涉及功能 |
资金贡献 | contribute() |
创建提案权限 | createProposal() |
投票流程与限制 | vote() |
提案执行条件 | executeProposal() |