第三章:智能合约设计与开发
🚀 引言
智能合约是去中心化应用的核心,它们定义了应用的业务逻辑和规则。在本章中,我们将设计并实现一个去中心化投票系统的智能合约。我们将从基本概念开始,逐步构建一个功能完整、安全可靠的投票系统。
想象一下,我们正在为一个社区、组织或公司创建一个透明的投票系统,让所有成员都能参与决策过程,并且每个人都能验证投票的公正性。这就是我们的目标!
📝 投票系统需求分析
在开始编码之前,让我们先明确我们的投票系统需要满足哪些需求:
功能需求
- 创建投票:管理员可以创建新的投票议题
- 添加候选人/选项:为每个投票添加可选项
- 投票权管理:控制谁有权参与投票
- 投票:允许有投票权的用户进行投票
- 查询结果:任何人都可以查看投票结果
- 时间控制:设定投票的开始和结束时间
非功能需求
- 安全性:防止重复投票、投票篡改等
- 透明性:所有操作公开透明
- 效率:优化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 finalized
expect(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 count
const 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 started
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");
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 ended
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);
// Manipulate time to allow voting (in a real test, we would use evm_increaseTime)
// For simplicity, we're just setting the times in the past
await 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 candidates
expect(results[1][0]).to.equal(2); // candidate 0 has 2 votes
expect(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 vote
await 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的基础知识,包括数据类型、函数修饰符和事件
- 设计了投票系统的数据结构,包括投票议题、候选人和投票记录
- 实现了完整的投票合约,包括创建投票、添加候选人、管理投票权、投票和查询结果等功能
- 考虑了安全性问题,并采取了相应的措施
- 编写了测试用例,确保合约的正确性和安全性
- 创建了部署脚本,方便部署合约到区块链网络
我们的投票系统合约现在已经准备好了,它提供了一个透明、安全的方式来进行去中心化投票。在下一章中,我们将开发前端界面,让用户可以通过浏览器与我们的智能合约交互。
🔍 进一步探索
如果你想进一步扩展这个投票系统,可以考虑以下功能:
- 秘密投票:实现零知识证明,让投票过程更加私密
- 代理投票:允许用户将投票权委托给其他人
- 多选投票:允许用户选择多个选项
- 加权投票:根据用户持有的代币数量或其他因素给予不同的投票权重
- 投票激励:为参与投票的用户提供奖励
准备好了吗?让我们继续第四章:前端开发与用户界面!