去中心化投票系统开发教程 第三章:智能合约设计与开发
第三章:智能合约设计与开发
🚀 引言
智能合约是去中心化应用的核心,它们定义了应用的业务逻辑和规则。在本章中,我们将设计并实现一个去中心化投票系统的智能合约。我们将从基本概念开始,逐步构建一个功能完整、安全可靠的投票系统。
想象一下,我们正在为一个社区、组织或公司创建一个透明的投票系统,让所有成员都能参与决策过程,并且每个人都能验证投票的公正性。这就是我们的目标!
📝 投票系统需求分析
在开始编码之前,让我们先明确我们的投票系统需要满足哪些需求:
功能需求
- 创建投票:管理员可以创建新的投票议题
- 添加候选人/选项:为每个投票添加可选项
- 投票权管理:控制谁有权参与投票
- 投票:允许有投票权的用户进行投票
- 查询结果:任何人都可以查看投票结果
- 时间控制:设定投票的开始和结束时间
非功能需求
- 安全性:防止重复投票、投票篡改等
- 透明性:所有操作公开透明
- 效率:优化Gas消耗
- 可用性:简单易用的接口
🔍 Solidity语言基础
在深入投票合约之前,让我们先快速回顾一下Solidity的基础知识。
Solidity是什么?
Solidity是一种面向对象的高级编程语言,专门用于实现智能合约。它的语法类似于JavaScript,但有一些重要的区别和特性。
合约结构
一个基本的Solidity合约结构如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;contract MyContract {// 状态变量uint public myVariable;// 事件event ValueChanged(uint oldValue, uint newValue);// 构造函数constructor(uint initialValue) {myVariable = initialValue;}// 函数function setValue(uint _newValue) public {uint oldValue = myVariable;myVariable = _newValue;emit ValueChanged(oldValue, _newValue);}
}
数据类型
Solidity支持多种数据类型:
-
值类型:
bool
:布尔值(true/false)int
/uint
:有符号/无符号整数(不同位数,如uint8, uint256)address
:以太坊地址(20字节)bytes
:字节数组enum
:枚举类型
-
引用类型:
string
:字符串array
:数组(固定大小或动态)struct
:结构体mapping
:键值映射(类似哈希表)
函数修饰符
Solidity中的函数可以有不同的可见性和状态修饰符:
-
可见性:
public
:任何人都可以调用private
:只能在合约内部调用internal
:只能在合约内部和继承合约中调用external
:只能从合约外部调用
-
状态修饰符:
view
:不修改状态(只读取)pure
:不读取也不修改状态payable
:可以接收以太币
自定义修饰符
Solidity允许创建自定义修饰符,用于在函数执行前后添加条件检查:
modifier onlyOwner() {require(msg.sender == owner, "Not the owner");_; // 继续执行函数主体
}function restrictedFunction() public onlyOwner {// 只有合约拥有者才能执行的代码
}
事件
事件用于记录合约中发生的重要操作,前端应用可以监听这些事件:
event Transfer(address indexed from, address indexed to, uint amount);function transfer(address to, uint amount) public {// 转账逻辑emit Transfer(msg.sender, to, amount);
}
🏗️ 投票合约设计
现在,让我们开始设计我们的投票系统合约。我们将采用模块化的方法,将系统分解为几个关键组件。
数据结构设计
首先,我们需要定义投票系统的核心数据结构:
// 投票议题
struct Ballot {uint id;string title;string description;uint startTime;uint endTime;bool finalized;address creator;
}// 候选人/选项
struct Candidate {uint id;string name;string info;uint voteCount;
}// 投票记录
struct Vote {address voter;uint candidateId;uint timestamp;
}
状态变量
接下来,我们需要定义合约的状态变量来存储这些数据:
// 存储所有投票议题
mapping(uint => Ballot) public ballots;
uint public ballotCount;// 存储每个投票议题的候选人
mapping(uint => mapping(uint => Candidate)) public candidates;
mapping(uint => uint) public candidateCounts;// 记录谁已经投过票
mapping(uint => mapping(address => bool)) public hasVoted;// 存储投票记录
mapping(uint => Vote[]) public votes;// 投票权管理
mapping(address => bool) public voters;
uint public voterCount;// 合约拥有者
address public owner;
事件定义
我们需要定义一些事件来记录重要操作:
event BallotCreated(uint ballotId, string title, address creator);
event CandidateAdded(uint ballotId, uint candidateId, string name);
event VoterAdded(address voter);
event VoteCast(uint ballotId, address voter, uint candidateId);
event BallotFinalized(uint ballotId, uint winningCandidateId);
💻 实现投票合约
现在,让我们开始实现我们的投票合约。创建一个新文件contracts/VotingSystem.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";/*** @title 去中心化投票系统* @dev 一个基于区块链的透明投票系统*/
contract VotingSystem is Ownable, ReentrancyGuard {// 数据结构struct Ballot {uint id;string title;string description;uint startTime;uint endTime;bool finalized;address creator;}struct Candidate {uint id;string name;string info;uint voteCount;}struct Vote {address voter;uint candidateId;uint timestamp;}// 状态变量mapping(uint => Ballot) public ballots;uint public ballotCount;mapping(uint => mapping(uint => Candidate)) public candidates;mapping(uint => uint) public candidateCounts;mapping(uint => mapping(address => bool)) public hasVoted;mapping(uint => Vote[]) private votes;mapping(address => bool) public voters;uint public voterCount;// 事件event BallotCreated(uint ballotId, string title, address creator);event CandidateAdded(uint ballotId, uint candidateId, string name);event VoterAdded(address voter);event VoteCast(uint ballotId, address voter, uint candidateId);event BallotFinalized(uint ballotId, uint winningCandidateId);/*** @dev 构造函数*/constructor() {// 合约部署者自动成为管理员}/*** @dev 创建新的投票议题* @param _title 投票标题* @param _description 投票描述* @param _startTime 开始时间(Unix时间戳)* @param _endTime 结束时间(Unix时间戳)*/function createBallot(string memory _title,string memory _description,uint _startTime,uint _endTime) public onlyOwner {require(_startTime >= block.timestamp, "Start time must be in the future");require(_endTime > _startTime, "End time must be after start time");uint ballotId = ballotCount++;ballots[ballotId] = Ballot({id: ballotId,title: _title,description: _description,startTime: _startTime,endTime: _endTime,finalized: false,creator: msg.sender});emit BallotCreated(ballotId, _title, msg.sender);}/*** @dev 为投票添加候选人/选项* @param _ballotId 投票ID* @param _name 候选人名称* @param _info 候选人信息*/function addCandidate(uint _ballotId,string memory _name,string memory _info) public onlyOwner {require(_ballotId < ballotCount, "Ballot does not exist");require(block.timestamp < ballots[_ballotId].startTime, "Voting has already started");uint candidateId = candidateCounts[_ballotId]++;candidates[_ballotId][candidateId] = Candidate({id: candidateId,name: _name,info: _info,voteCount: 0});emit CandidateAdded(_ballotId, candidateId, _name);}/*** @dev 添加有投票权的用户* @param _voter 用户地址*/function addVoter(address _voter) public onlyOwner {require(!voters[_voter], "Address is already a voter");voters[_voter] = true;voterCount++;emit VoterAdded(_voter);}/*** @dev 批量添加有投票权的用户* @param _voters 用户地址数组*/function addVoters(address[] memory _voters) public onlyOwner {for (uint i = 0; i < _voters.length; i++) {if (!voters[_voters[i]]) {voters[_voters[i]] = true;voterCount++;emit VoterAdded(_voters[i]);}}}/*** @dev 投票* @param _ballotId 投票ID* @param _candidateId 候选人ID*/function vote(uint _ballotId, uint _candidateId) public nonReentrant {require(voters[msg.sender], "You don't have voting rights");require(_ballotId < ballotCount, "Ballot does not exist");require(_candidateId < candidateCounts[_ballotId], "Candidate does not exist");require(!hasVoted[_ballotId][msg.sender], "You have already voted in this ballot");Ballot storage ballot = ballots[_ballotId];require(block.timestamp >= ballot.startTime, "Voting has not started yet");require(block.timestamp <= ballot.endTime, "Voting has ended");require(!ballot.finalized, "Ballot has been finalized");// 记录投票hasVoted[_ballotId][msg.sender] = true;// 增加候选人票数candidates[_ballotId][_candidateId].voteCount++;// 存储投票记录votes[_ballotId].push(Vote({voter: msg.sender,candidateId: _candidateId,timestamp: block.timestamp}));emit VoteCast(_ballotId, msg.sender, _candidateId);}/*** @dev 获取投票结果* @param _ballotId 投票ID* @return 候选人ID数组和对应的票数数组*/function getBallotResults(uint _ballotId) public view returns (uint[] memory, uint[] memory) {require(_ballotId < ballotCount, "Ballot does not exist");uint candidateCount = candidateCounts[_ballotId];uint[] memory candidateIds = new uint[](candidateCount);uint[] memory voteCounts = new uint[](candidateCount);for (uint i = 0; i < candidateCount; i++) {candidateIds[i] = i;voteCounts[i] = candidates[_ballotId][i].voteCount;}return (candidateIds, voteCounts);}/*** @dev 获取投票的获胜者* @param _ballotId 投票ID* @return 获胜候选人ID*/function getWinner(uint _ballotId) public view returns (uint) {require(_ballotId < ballotCount, "Ballot does not exist");require(block.timestamp > ballots[_ballotId].endTime, "Voting has not ended yet");uint winningCandidateId = 0;uint winningVoteCount = 0;for (uint i = 0; i < candidateCounts[_ballotId]; i++) {if (candidates[_ballotId][i].voteCount > winningVoteCount) {winningVoteCount = candidates[_ballotId][i].voteCount;winningCandidateId = i;}}return winningCandidateId;}/*** @dev 结束投票并确认结果* @param _ballotId 投票ID*/function finalizeBallot(uint _ballotId) public onlyOwner {require(_ballotId < ballotCount, "Ballot does not exist");require(block.timestamp > ballots[_ballotId].endTime, "Voting has not ended yet");require(!ballots[_ballotId].finalized, "Ballot already finalized");uint winningCandidateId = getWinner(_ballotId);ballots[_ballotId].finalized = true;emit BallotFinalized(_ballotId, winningCandidateId);}/*** @dev 获取投票的详细信息* @param _ballotId 投票ID* @return 投票标题、描述、开始时间、结束时间、是否已结束、创建者*/function getBallotDetails(uint _ballotId) public view returns (string memory,string memory,uint,uint,bool,address) {require(_ballotId < ballotCount, "Ballot does not exist");Ballot storage ballot = ballots[_ballotId];return (ballot.title,ballot.description,ballot.startTime,ballot.endTime,ballot.finalized,ballot.creator);}/*** @dev 获取候选人详细信息* @param _ballotId 投票ID* @param _candidateId 候选人ID* @return 候选人名称、信息、票数*/function getCandidateDetails(uint _ballotId, uint _candidateId) public view returns (string memory,string memory,uint) {require(_ballotId < ballotCount, "Ballot does not exist");require(_candidateId < candidateCounts[_ballotId], "Candidate does not exist");Candidate storage candidate = candidates[_ballotId][_candidateId];return (candidate.name,candidate.info,candidate.voteCount);}/*** @dev 检查用户是否有投票权* @param _voter 用户地址* @return 是否有投票权*/function hasVotingRights(address _voter) public view returns (bool) {return voters[_voter];}/*** @dev 检查用户是否已在特定投票中投票* @param _ballotId 投票ID* @param _voter 用户地址* @return 是否已投票*/function hasVotedInBallot(uint _ballotId, address _voter) public view returns (bool) {return hasVoted[_ballotId][_voter];}
}
🔒 安全考虑
在开发智能合约时,安全性是最重要的考虑因素之一。我们的合约已经包含了一些安全措施:
- 访问控制:使用OpenZeppelin的
Ownable
合约确保只有合约拥有者可以执行某些操作 - 重入攻击防护:使用
ReentrancyGuard
防止重入攻击 - 条件检查:使用
require
语句验证所有操作的前置条件 - 时间控制:确保投票只能在指定的时间范围内进行
但我们还可以考虑更多的安全措施:
防止前端运行攻击
在以太坊网络中,交易在被打包进区块前是公开的,这可能导致前端运行攻击。对于投票系统,这可能不是主要问题,但在其他应用中需要考虑。
整数溢出保护
Solidity 0.8.0及以上版本已经内置了整数溢出检查,但如果使用较低版本,应该使用SafeMath库。
权限分离
我们可以实现更细粒度的权限控制,例如区分管理员和投票创建者的角色。
🧪 测试合约
测试是确保合约正确性和安全性的关键步骤。让我们创建一个测试文件test/VotingSystem.test.js
:
const { expect } = require("chai");
const { ethers } = require("hardhat");describe("VotingSystem", function () {let VotingSystem;let votingSystem;let owner;let addr1;let addr2;let addrs;beforeEach(async function () {// 获取合约工厂和签名者VotingSystem = await ethers.getContractFactory("VotingSystem");[owner, addr1, addr2, ...addrs] = await ethers.getSigners();// 部署合约votingSystem = await VotingSystem.deploy();await votingSystem.deployed();});describe("Deployment", function () {it("Should set the right owner", async function () {expect(await votingSystem.owner()).to.equal(owner.address);});it("Should have zero ballots initially", async function () {expect(await votingSystem.ballotCount()).to.equal(0);});});describe("Ballot Management", function () {it("Should create a new ballot", async function () {const now = Math.floor(Date.now() / 1000);const startTime = now + 100;const endTime = now + 1000;await votingSystem.createBallot("Test Ballot","This is a test ballot",startTime,endTime);expect(await votingSystem.ballotCount()).to.equal(1);const ballotDetails = await votingSystem.getBallotDetails(0);expect(ballotDetails[0]).to.equal("Test Ballot");expect(ballotDetails[1]).to.equal("This is a test ballot");expect(ballotDetails[2]).to.equal(startTime);expect(ballotDetails[3]).to.equal(endTime);expect(ballotDetails[4]).to.equal(false); // not finalizedexpect(ballotDetails[5]).to.equal(owner.address);});it("Should add candidates to a ballot", async function () {const now = Math.floor(Date.now() / 1000);const startTime = now + 100;const endTime = now + 1000;await votingSystem.createBallot("Test Ballot","This is a test ballot",startTime,endTime);await votingSystem.addCandidate(0, "Candidate 1", "Info 1");await votingSystem.addCandidate(0, "Candidate 2", "Info 2");expect(await votingSystem.candidateCounts(0)).to.equal(2);const candidate1 = await votingSystem.getCandidateDetails(0, 0);expect(candidate1[0]).to.equal("Candidate 1");expect(candidate1[1]).to.equal("Info 1");expect(candidate1[2]).to.equal(0); // vote countconst candidate2 = await votingSystem.getCandidateDetails(0, 1);expect(candidate2[0]).to.equal("Candidate 2");expect(candidate2[1]).to.equal("Info 2");expect(candidate2[2]).to.equal(0); // vote count});});describe("Voter Management", function () {it("Should add a voter", async function () {await votingSystem.addVoter(addr1.address);expect(await votingSystem.voters(addr1.address)).to.equal(true);expect(await votingSystem.voterCount()).to.equal(1);});it("Should add multiple voters", async function () {await votingSystem.addVoters([addr1.address, addr2.address]);expect(await votingSystem.voters(addr1.address)).to.equal(true);expect(await votingSystem.voters(addr2.address)).to.equal(true);expect(await votingSystem.voterCount()).to.equal(2);});});describe("Voting Process", function () {beforeEach(async function () {const now = Math.floor(Date.now() / 1000);const startTime = now - 100; // voting has startedconst endTime = now + 1000;await votingSystem.createBallot("Test Ballot","This is a test ballot",startTime,endTime);await votingSystem.addCandidate(0, "Candidate 1", "Info 1");await votingSystem.addCandidate(0, "Candidate 2", "Info 2");await votingSystem.addVoter(addr1.address);await votingSystem.addVoter(addr2.address);});it("Should allow a voter to vote", async function () {await votingSystem.connect(addr1).vote(0, 0);expect(await votingSystem.hasVotedInBallot(0, addr1.address)).to.equal(true);const candidate = await votingSystem.getCandidateDetails(0, 0);expect(candidate[2]).to.equal(1); // vote count});it("Should not allow double voting", async function () {await votingSystem.connect(addr1).vote(0, 0);await expect(votingSystem.connect(addr1).vote(0, 1)).to.be.revertedWith("You have already voted in this ballot");});it("Should not allow non-voters to vote", async function () {await expect(votingSystem.connect(addrs[0]).vote(0, 0)).to.be.revertedWith("You don't have voting rights");});});describe("Results and Finalization", function () {beforeEach(async function () {const now = Math.floor(Date.now() / 1000);const startTime = now - 200;const endTime = now - 100; // voting has endedawait votingSystem.createBallot("Test Ballot","This is a test ballot",startTime,endTime);await votingSystem.addCandidate(0, "Candidate 1", "Info 1");await votingSystem.addCandidate(0, "Candidate 2", "Info 2");await votingSystem.addVoter(addr1.address);await votingSystem.addVoter(addr2.address);// Manipulate time to allow voting (in a real test, we would use evm_increaseTime)// For simplicity, we're just setting the times in the pastawait votingSystem.connect(addr1).vote(0, 0);await votingSystem.connect(addr2).vote(0, 0);});it("Should return correct ballot results", async function () {const results = await votingSystem.getBallotResults(0);expect(results[0].length).to.equal(2); // two candidatesexpect(results[1][0]).to.equal(2); // candidate 0 has 2 votesexpect(results[1][1]).to.equal(0); // candidate 1 has 0 votes});it("Should identify the correct winner", async function () {const winner = await votingSystem.getWinner(0);expect(winner).to.equal(0); // candidate 0 is the winner});it("Should finalize the ballot", async function () {await votingSystem.finalizeBallot(0);const ballotDetails = await votingSystem.getBallotDetails(0);expect(ballotDetails[4]).to.equal(true); // finalized});it("Should not allow voting after finalization", async function () {await votingSystem.finalizeBallot(0);// Try to add a new voter and have them voteawait votingSystem.addVoter(addrs[0].address);await expect(votingSystem.connect(addrs[0]).vote(0, 1)).to.be.revertedWith("Ballot has been finalized");});});
});
要运行测试,使用以下命令:
npx hardhat test
🚀 部署脚本
让我们创建一个部署脚本scripts/deploy.js
:
const hre = require("hardhat");async function main() {// 获取合约工厂const VotingSystem = await hre.ethers.getContractFactory("VotingSystem");// 部署合约const votingSystem = await VotingSystem.deploy();await votingSystem.deployed();console.log("VotingSystem deployed to:", votingSystem.address);// 创建一个示例投票(可选)const now = Math.floor(Date.now() / 1000);const startTime = now + 60; // 1分钟后开始const endTime = now + 3600; // 1小时后结束await votingSystem.createBallot("示例投票","这是一个示例投票,用于测试系统功能",startTime,endTime);console.log("Example ballot created");// 添加候选人await votingSystem.addCandidate(0, "选项A", "这是选项A的描述");await votingSystem.addCandidate(0, "选项B", "这是选项B的描述");console.log("Candidates added");
}main().then(() => process.exit(0)).catch((error) => {console.error(error);process.exit(1);});
要部署合约,使用以下命令:
npx hardhat run scripts/deploy.js --network localhost
📝 小结
在本章中,我们:
- 分析了投票系统的需求,明确了功能和非功能需求
- 回顾了Solidity的基础知识,包括数据类型、函数修饰符和事件
- 设计了投票系统的数据结构,包括投票议题、候选人和投票记录
- 实现了完整的投票合约,包括创建投票、添加候选人、管理投票权、投票和查询结果等功能
- 考虑了安全性问题,并采取了相应的措施
- 编写了测试用例,确保合约的正确性和安全性
- 创建了部署脚本,方便部署合约到区块链网络
我们的投票系统合约现在已经准备好了,它提供了一个透明、安全的方式来进行去中心化投票。在下一章中,我们将开发前端界面,让用户可以通过浏览器与我们的智能合约交互。
🔍 进一步探索
如果你想进一步扩展这个投票系统,可以考虑以下功能:
- 秘密投票:实现零知识证明,让投票过程更加私密
- 代理投票:允许用户将投票权委托给其他人
- 多选投票:允许用户选择多个选项
- 加权投票:根据用户持有的代币数量或其他因素给予不同的投票权重
- 投票激励:为参与投票的用户提供奖励
准备好了吗?让我们继续第四章:前端开发与用户界面!