跳转到内容

去中心化投票系统

去中心化投票系统

本页内容正在整理中,欢迎贡献

概述

本教程带你从零构建一个链上投票系统。学习目标:理解 Solidity 的 mappingstructevent 和访问控制,构建一个支持创建提案、投票和统计结果的完整合约。

主要内容

需求分析

功能需求:
✅ 管理员(Owner)可以创建投票提案
✅ 每个地址只能投票一次
✅ 支持多个选项(Option A / B / C...)
✅ 投票有时间限制(deadline)
✅ 任何人可以查询当前投票结果
✅ 投票结束后结果不可更改

合约实现

// SPDX-License-Identifier: MIT
pragma 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];
}
}

测试

test/Voting.test.ts
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");
});
});

部署与验证

Terminal window
# 部署到 Sepolia
npx hardhat run scripts/deploy.ts --network sepolia
# 验证合约
npx hardhat verify --network sepolia <合约地址>

扩展想法

  • 添加 ERC-20 代币投票权重(Token Weighted Voting)
  • 委托投票(Delegation,参考 Compound GovernorBravo)
  • 快照机制(防止购买代币后立即投票)
  • 多轮投票 / 排名投票

深入阅读