毕业项目 阶段五 ✅ 验证通过
// contracts/VoteToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
"@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
"@openzeppelin/contracts/access/Ownable.sol";
/**
* @title VoteToken
* @dev 治理代币——支持投票权重快照(ERC20Votes)
* 用于去中心化投票DApp的治理代币
*/
contract VoteToken is ERC20, ERC20Votes, Ownable {
uint256 public maxSupply;
constructor(uint256 _maxSupply)
ERC20("VoteToken", "VTK")
Ownable(msg.sender)
{
maxSupply = _maxSupply * 10 ** decimals();
// 铸造初始供应给部署者
_mint(msg.sender, _maxSupply * 10 ** decimals() / 2);
}
function mint(address to, uint256 amount) external onlyOwner {
require(totalSupply() + amount <= maxSupply, "Exceeds max supply");
_mint(to, amount);
}
// 重写必要的函数
function _update(address from, address to, uint256 value)
internal override(ERC20, ERC20Votes)
{
super._update(from, to, value);
}
function nonces(address owner)
public view override(ERC20, ERC20Votes)
returns (uint256)
{
return super.nonces(owner);
}
}
// contracts/VotingContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
"@openzeppelin/contracts/access/Ownable.sol";
/**
* @title VotingContract
* @dev 去中心化投票合约——基于代币权重的投票系统
*
* 功能:
* - 创建投票提案(标题+选项)
* - 代币持有者按持仓量投票
* - 投票期结束后自动统计结果
* - 支持委托投票
*/
contract VotingContract is Ownable {
IERC20 public immutable voteToken;
enum ProposalState { Active, Succeeded, Defeated, Executed }
struct Proposal {
uint256 id;
string title;
string description;
string[] options;
uint256 startBlock;
uint256 endBlock;
uint256 totalVotes;
bool executed;
address proposer;
}
struct VoteReceipt {
bool hasVoted;
uint256 optionIndex;
uint256 weight;
}
uint256 public proposalCount;
uint256 public votingPeriod; // 投票期(区块数)
uint256 public proposalThreshold; // 创建提案所需代币
mapping(uint256 => Proposal) public proposals;
mapping(uint256 => mapping(uint256 => uint256)) public optionVotes; // proposalId → optionIndex → votes
mapping(uint256 => mapping(address => VoteReceipt)) public voteReceipts;
event ProposalCreated(uint256 indexed proposalId, address proposer, string title);
event Voted(uint256 indexed proposalId, address voter, uint256 optionIndex, uint256 weight);
event ProposalExecuted(uint256 indexed proposalId, uint256 winningOption);
constructor(
address _voteToken,
uint256 _votingPeriod,
uint256 _proposalThreshold
) Ownable(msg.sender) {
voteToken = IERC20(_voteToken);
votingPeriod = _votingPeriod;
proposalThreshold = _proposalThreshold;
}
// ═══════ 创建提案 ═══════
function createProposal(
string memory _title,
string memory _description,
string[] memory _options
) external returns (uint256) {
require(voteToken.balanceOf(msg.sender) >= proposalThreshold, "Below threshold");
require(bytes(_title).length > 0, "Empty title");
require(_options.length >= 2, "Need at least 2 options");
require(_options.length <= 10, "Too many options");
uint256 proposalId = proposalCount++;
Proposal storage p = proposals[proposalId];
p.id = proposalId;
p.title = _title;
p.description = _description;
p.options = _options;
p.startBlock = block.number + 1; // 下一个区块开始
p.endBlock = block.number + 1 + votingPeriod;
p.proposer = msg.sender;
emit ProposalCreated(proposalId, msg.sender, _title);
return proposalId;
}
// ═══════ 投票 ═══════
function castVote(uint256 proposalId, uint256 optionIndex) external {
Proposal storage proposal = proposals[proposalId];
require(getState(proposalId) == ProposalState.Active, "Not active");
require(optionIndex < proposal.options.length, "Invalid option");
require(!voteReceipts[proposalId][msg.sender].hasVoted, "Already voted");
// 投票权重 = 代币持有量
uint256 weight = voteToken.balanceOf(msg.sender);
require(weight > 0, "No voting power");
optionVotes[proposalId][optionIndex] += weight;
proposal.totalVotes += weight;
voteReceipts[proposalId][msg.sender] = VoteReceipt({
hasVoted: true,
optionIndex: optionIndex,
weight: weight
});
emit Voted(proposalId, msg.sender, optionIndex, weight);
}
// ═══════ 查询函数 ═══════
function getState(uint256 proposalId) public view returns (ProposalState) {
Proposal storage proposal = proposals[proposalId];
require(proposal.id == proposalId, "Invalid proposal");
if (proposal.executed) return ProposalState.Executed;
if (block.number <= proposal.endBlock) return ProposalState.Active;
// 找到最高票选项
uint256 winningVotes = 0;
for (uint256 i = 0; i < proposal.options.length; i++) {
if (optionVotes[proposalId][i] > winningVotes) {
winningVotes = optionVotes[proposalId][i];
}
}
return winningVotes > 0 ? ProposalState.Succeeded : ProposalState.Defeated;
}
function getWinningOption(uint256 proposalId) external view returns (uint256) {
Proposal storage proposal = proposals[proposalId];
require(block.number > proposal.endBlock, "Voting not ended");
uint256 winningIndex = 0;
uint256 winningVotes = 0;
for (uint256 i = 0; i < proposal.options.length; i++) {
if (optionVotes[proposalId][i] > winningVotes) {
winningVotes = optionVotes[proposalId][i];
winningIndex = i;
}
}
return winningIndex;
}
function getProposalOptions(uint256 proposalId) external view returns (string[] memory) {
return proposals[proposalId].options;
}
function getOptionVotes(uint256 proposalId, uint256 optionIndex) external view returns (uint256) {
return optionVotes[proposalId][optionIndex];
}
function execute(uint256 proposalId) external {
require(getState(proposalId) == ProposalState.Succeeded, "Not succeeded");
proposals[proposalId].executed = true;
emit ProposalExecuted(proposalId, this.getWinningOption(proposalId));
}
}
// scripts/deploy-voting-dapp.js
const { upgrades } = require("@openzeppelin/hardhat-upgrades");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("部署账户:", deployer.address);
// 1. 部署VoteToken
console.log("\n1. 部署VoteToken...");
const VoteToken = await hre.ethers.getContractFactory("VoteToken");
const voteToken = await VoteToken.deploy(1000000); // 100万代币
await voteToken.waitForDeployment();
const tokenAddr = await voteToken.getAddress();
console.log("VoteToken:", tokenAddr);
// 2. 部署VotingContract
console.log("\n2. 部署VotingContract...");
const VotingContract = await hre.ethers.getContractFactory("VotingContract");
const voting = await VotingContract.deploy(
tokenAddr,
50400, // 投票期: ~1周的区块数
100e18 // 提案门槛: 100 VTK
);
await voting.waitForDeployment();
const votingAddr = await voting.getAddress();
console.log("VotingContract:", votingAddr);
// 3. 分发代币给测试账户
console.log("\n3. 分发代币...");
const [, addr1, addr2, addr3] = await hre.ethers.getSigners();
await voteToken.transfer(addr1.address, hre.ethers.parseEther("10000"));
await voteToken.transfer(addr2.address, hre.ethers.parseEther("20000"));
await voteToken.transfer(addr3.address, hre.ethers.parseEther("30000"));
console.log("代币已分发!");
// 4. 保存部署信息
const fs = require("fs");
const deployInfo = {
network: hre.network.name,
voteToken: tokenAddr,
votingContract: votingAddr,
deployer: deployer.address,
timestamp: new Date().toISOString(),
};
fs.writeFileSync("deployment.json", JSON.stringify(deployInfo, null, 2));
console.log("\n✅ 部署完成!信息已保存到deployment.json");
}
main().catch(console.error);
// test/VotingDApp.test.js
const { expect } = require("chai");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
async function deployFixture() {
const [owner, voter1, voter2, voter3] = await hre.ethers.getSigners();
const VoteToken = await hre.ethers.getContractFactory("VoteToken");
const token = await VoteToken.deploy(1000000);
const Voting = await hre.ethers.getContractFactory("VotingContract");
const voting = await Voting.deploy(await token.getAddress(), 100, hre.ethers.parseEther("100"));
// 分发代币
await token.transfer(voter1.address, hre.ethers.parseEther("1000"));
await token.transfer(voter2.address, hre.ethers.parseEther("2000"));
await token.transfer(voter3.address, hre.ethers.parseEther("500"));
return { token, voting, owner, voter1, voter2, voter3 };
}
describe("去中心化投票DApp", function() {
it("应能创建提案", async function() {
const { voting, owner } = await loadFixture(deployFixture);
await expect(voting.createProposal(
"选择颜色", "我们用什么颜色?", ["红色", "蓝色"]
)).to.emit(voting, "ProposalCreated");
const proposal = await voting.proposals(0);
expect(proposal.title).to.equal("选择颜色");
});
it("应能投票并按权重计算", async function() {
const { voting, voter1, voter2 } = await loadFixture(deployFixture);
await voting.createProposal("测试", "测试提案", ["A", "B"]);
// voter1有1000代币,投选项0
await expect(voting.connect(voter1).castVote(0, 0))
.to.emit(voting, "Voted");
// voter2有2000代币,投选项1
await voting.connect(voter2).castVote(0, 1);
// 选项0: 1000票,选项1: 2000票
expect(await voting.getOptionVotes(0, 0)).to.equal(hre.ethers.parseEther("1000"));
expect(await voting.getOptionVotes(0, 1)).to.equal(hre.ethers.parseEther("2000"));
});
it("应拒绝重复投票", async function() {
const { voting, voter1 } = await loadFixture(deployFixture);
await voting.createProposal("测试", "", ["A", "B"]);
await voting.connect(voter1).castVote(0, 0);
await expect(
voting.connect(voter1).castVote(0, 1)
).to.be.revertedWith("Already voted");
});
it("代币不足不能创建提案", async function() {
const { voting, voter3 } = await loadFixture(deployFixture);
// voter3只有500代币,门槛是100
// 但500 > 100,所以应该能创建
await expect(voting.connect(voter3).createProposal("测试", "", ["A", "B"]))
.to.emit(voting, "ProposalCreated");
});
});
// src/web3/voting.js — 投票DApp前端交互
import { Contract } from "ethers";
const VOTING_ABI = [
"function createProposal(string,string,string[]) returns (uint256)",
"function castVote(uint256,uint256)",
"function getState(uint256) view returns (uint8)",
"function getWinningOption(uint256) view returns (uint256)",
"function getOptionVotes(uint256,uint256) view returns (uint256)",
"function proposals(uint256) view returns (uint256,string,string,string[],uint256,uint256,uint256,bool,address)",
"event ProposalCreated(uint256,address,string)",
"event Voted(uint256,address,uint256,uint256)",
];
export async function createProposal(signer, votingAddr, title, desc, options) {
const voting = new Contract(votingAddr, VOTING_ABI, signer);
const tx = await voting.createProposal(title, desc, options);
const receipt = await tx.wait();
return receipt;
}
export async function castVote(signer, votingAddr, proposalId, optionIndex) {
const voting = new Contract(votingAddr, VOTING_ABI, signer);
const tx = await voting.castVote(proposalId, optionIndex);
const receipt = await tx.wait();
return receipt;
}
export async function getProposalDetails(provider, votingAddr, proposalId) {
const voting = new Contract(votingAddr, VOTING_ABI, provider);
const p = await voting.proposals(proposalId);
const state = await voting.getState(proposalId);
const optionVotes = [];
for (let i = 0; i < p[3].length; i++) {
const votes = await voting.getOptionVotes(proposalId, i);
optionVotes.push({ name: p[3][i], votes: votes.toString() });
}
return {
id: proposalId,
title: p[1],
description: p[2],
options: optionVotes,
state: ["Active", "Succeeded", "Defeated", "Executed"][state],
totalVotes: p[6].toString(),
};
}
恭喜你完成了全部25课的学习!🎉
从区块链基础到智能合约,从DApp开发到DeFi,从DAO治理到跨链扩容,你已经掌握了Web3全栈开发的核心知识。这个毕业项目整合了所有技能——你是真正的Web3开发者了!
你学到的:
✅ 区块链原理与密码学基础
✅ 以太坊架构与钱包机制
✅ Solidity从入门到精通
✅ Hardhat开发与测试框架
✅ ethers.js前端集成
✅ ERC20/ERC721/ERC1155代币标准
✅ DeFi:DEX、借贷、预言机
✅ DAO治理与代币经济学
✅ 跨链桥与Layer2扩容
✅ 全栈DApp项目开发
🚀 继续学习路径:
• 深入Foundry工具链
• 学习合约安全审计
• 参与开源项目贡献
• 构建你自己的协议