去中心化投票系统开发教程 第三章:智能合约设计与开发

发布于:2025-09-07 ⋅ 阅读:(10) ⋅ 点赞:(0)

第三章:智能合约设计与开发

在这里插入图片描述

🚀 引言

智能合约是去中心化应用的核心,它们定义了应用的业务逻辑和规则。在本章中,我们将设计并实现一个去中心化投票系统的智能合约。我们将从基本概念开始,逐步构建一个功能完整、安全可靠的投票系统。

想象一下,我们正在为一个社区、组织或公司创建一个透明的投票系统,让所有成员都能参与决策过程,并且每个人都能验证投票的公正性。这就是我们的目标!

📝 投票系统需求分析

在开始编码之前,让我们先明确我们的投票系统需要满足哪些需求:

功能需求

  1. 创建投票:管理员可以创建新的投票议题
  2. 添加候选人/选项:为每个投票添加可选项
  3. 投票权管理:控制谁有权参与投票
  4. 投票:允许有投票权的用户进行投票
  5. 查询结果:任何人都可以查看投票结果
  6. 时间控制:设定投票的开始和结束时间

非功能需求

  1. 安全性:防止重复投票、投票篡改等
  2. 透明性:所有操作公开透明
  3. 效率:优化Gas消耗
  4. 可用性:简单易用的接口

🔍 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支持多种数据类型:

  1. 值类型

    • bool:布尔值(true/false)
    • int/uint:有符号/无符号整数(不同位数,如uint8, uint256)
    • address:以太坊地址(20字节)
    • bytes:字节数组
    • enum:枚举类型
  2. 引用类型

    • 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];
    }
}

🔒 安全考虑

在开发智能合约时,安全性是最重要的考虑因素之一。我们的合约已经包含了一些安全措施:

  1. 访问控制:使用OpenZeppelin的Ownable合约确保只有合约拥有者可以执行某些操作
  2. 重入攻击防护:使用ReentrancyGuard防止重入攻击
  3. 条件检查:使用require语句验证所有操作的前置条件
  4. 时间控制:确保投票只能在指定的时间范围内进行

但我们还可以考虑更多的安全措施:

防止前端运行攻击

在以太坊网络中,交易在被打包进区块前是公开的,这可能导致前端运行攻击。对于投票系统,这可能不是主要问题,但在其他应用中需要考虑。

整数溢出保护

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

📝 小结

在本章中,我们:

  1. 分析了投票系统的需求,明确了功能和非功能需求
  2. 回顾了Solidity的基础知识,包括数据类型、函数修饰符和事件
  3. 设计了投票系统的数据结构,包括投票议题、候选人和投票记录
  4. 实现了完整的投票合约,包括创建投票、添加候选人、管理投票权、投票和查询结果等功能
  5. 考虑了安全性问题,并采取了相应的措施
  6. 编写了测试用例,确保合约的正确性和安全性
  7. 创建了部署脚本,方便部署合约到区块链网络

我们的投票系统合约现在已经准备好了,它提供了一个透明、安全的方式来进行去中心化投票。在下一章中,我们将开发前端界面,让用户可以通过浏览器与我们的智能合约交互。

🔍 进一步探索

如果你想进一步扩展这个投票系统,可以考虑以下功能:

  1. 秘密投票:实现零知识证明,让投票过程更加私密
  2. 代理投票:允许用户将投票权委托给其他人
  3. 多选投票:允许用户选择多个选项
  4. 加权投票:根据用户持有的代币数量或其他因素给予不同的投票权重
  5. 投票激励:为参与投票的用户提供奖励

准备好了吗?让我们继续第四章:前端开发与用户界面


网站公告

今日签到

点亮在社区的每一天
去签到