去中心化投票系统
去中心化投票系统
本页内容正在整理中,欢迎贡献。
概述
本教程带你从零构建一个链上投票系统。学习目标:理解 Solidity 的 mapping、struct、event 和访问控制,构建一个支持创建提案、投票和统计结果的完整合约。
主要内容
需求分析
功能需求: ✅ 管理员(Owner)可以创建投票提案 ✅ 每个地址只能投票一次 ✅ 支持多个选项(Option A / B / C...) ✅ 投票有时间限制(deadline) ✅ 任何人可以查询当前投票结果 ✅ 投票结束后结果不可更改合约实现
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
contract Voting {
struct Proposal { string title; string description; uint256 deadline; bool executed; string[] options; mapping(uint256 => uint256) votes; // optionIndex => voteCount mapping(address => bool) hasVoted; }
uint256 public proposalCount; mapping(uint256 => Proposal) public proposals; address public owner;
event ProposalCreated(uint256 indexed proposalId, string title, uint256 deadline); event Voted(uint256 indexed proposalId, address indexed voter, uint256 optionIndex);
error NotOwner(); error ProposalNotFound(); error VotingClosed(); error AlreadyVoted(); error InvalidOption();
modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; }
constructor() { owner = msg.sender; }
// 创建提案 function createProposal( string calldata title, string calldata description, uint256 durationInSeconds, string[] calldata options ) external onlyOwner returns (uint256 proposalId) { require(options.length >= 2, "Need at least 2 options"); require(durationInSeconds > 0, "Duration must be positive");
proposalId = proposalCount++; Proposal storage proposal = proposals[proposalId]; proposal.title = title; proposal.description = description; proposal.deadline = block.timestamp + durationInSeconds;
for (uint256 i = 0; i < options.length; ) { proposal.options.push(options[i]); unchecked { ++i; } }
emit ProposalCreated(proposalId, title, proposal.deadline); }
// 投票 function vote(uint256 proposalId, uint256 optionIndex) external { Proposal storage proposal = proposals[proposalId];
if (proposalId >= proposalCount) revert ProposalNotFound(); if (block.timestamp > proposal.deadline) revert VotingClosed(); if (proposal.hasVoted[msg.sender]) revert AlreadyVoted(); if (optionIndex >= proposal.options.length) revert InvalidOption();
proposal.hasVoted[msg.sender] = true; proposal.votes[optionIndex]++;
emit Voted(proposalId, msg.sender, optionIndex); }
// 查询提案信息 function getProposal(uint256 proposalId) external view returns ( string memory title, string memory description, uint256 deadline, string[] memory options, uint256[] memory voteCounts, bool isActive ) { Proposal storage proposal = proposals[proposalId]; title = proposal.title; description = proposal.description; deadline = proposal.deadline; options = proposal.options; isActive = block.timestamp <= proposal.deadline;
voteCounts = new uint256[](proposal.options.length); for (uint256 i = 0; i < proposal.options.length; ) { voteCounts[i] = proposal.votes[i]; unchecked { ++i; } } }
// 查询获胜选项 function getWinner(uint256 proposalId) external view returns ( uint256 winnerIndex, string memory winnerOption, uint256 winnerVotes ) { require(block.timestamp > proposals[proposalId].deadline, "Voting not ended");
Proposal storage proposal = proposals[proposalId]; for (uint256 i = 0; i < proposal.options.length; ) { if (proposal.votes[i] > winnerVotes) { winnerVotes = proposal.votes[i]; winnerIndex = i; winnerOption = proposal.options[i]; } unchecked { ++i; } } }
// 查询是否已投票 function hasVoted(uint256 proposalId, address voter) external view returns (bool) { return proposals[proposalId].hasVoted[voter]; }}测试
import { expect } from "chai";import { ethers } from "hardhat";import { loadFixture, time } from "@nomicfoundation/hardhat-toolbox/network-helpers";
describe("Voting", function () { async function deployVotingFixture() { const [owner, alice, bob, carol] = await ethers.getSigners(); const Voting = await ethers.getContractFactory("Voting"); const voting = await Voting.deploy(); return { voting, owner, alice, bob, carol }; }
it("应该允许创建提案", async function () { const { voting, owner } = await loadFixture(deployVotingFixture);
await expect( voting.createProposal( "最佳 L2 方案", "选择你最喜欢的 L2", 3600, ["Arbitrum", "Optimism", "Base"] ) ).to.emit(voting, "ProposalCreated").withArgs(0, "最佳 L2 方案", await time.latest() + 3600 + 1); });
it("应该正确计票", async function () { const { voting, alice, bob, carol } = await loadFixture(deployVotingFixture);
await voting.createProposal("测试", "", 3600, ["A", "B"]);
await voting.connect(alice).vote(0, 0); // A await voting.connect(bob).vote(0, 1); // B await voting.connect(carol).vote(0, 0); // A
const [,,,, voteCounts] = await voting.getProposal(0); expect(voteCounts[0]).to.equal(2); // A 有 2 票 expect(voteCounts[1]).to.equal(1); // B 有 1 票 });
it("不能重复投票", async function () { const { voting, alice } = await loadFixture(deployVotingFixture); await voting.createProposal("测试", "", 3600, ["A", "B"]); await voting.connect(alice).vote(0, 0); await expect(voting.connect(alice).vote(0, 0)).to.be.revertedWithCustomError(voting, "AlreadyVoted"); });});部署与验证
# 部署到 Sepolianpx hardhat run scripts/deploy.ts --network sepolia
# 验证合约npx hardhat verify --network sepolia <合约地址>扩展想法
- 添加 ERC-20 代币投票权重(Token Weighted Voting)
- 委托投票(Delegation,参考 Compound GovernorBravo)
- 快照机制(防止购买代币后立即投票)
- 多轮投票 / 排名投票