(五)以太坊——从零构建一个可扩展的委托投票合约
1. 委托投票合约的核心设计思路
在DAO(去中心化自治组织)的治理中,委托投票是一种常见机制。想象一下公司股东大会的场景:股东可以亲自投票,也可以委托代理人行使投票权。区块链上的智能合约需要实现类似的逻辑,但要以代码形式确保以下特性:
- 去中心化:没有单一控制方,投票权分配和计票完全由算法执行
- 防篡改:投票记录一旦上链就无法修改
- 可验证:所有操作公开透明,任何人都能审计投票结果
这个合约的核心数据结构是两个结构体:
struct Voter { uint weight; // 投票权重 bool voted; // 是否已投票 address delegate; // 委托的代理人地址 uint vote; // 投票的提案索引 } struct Proposal { string name; // 提案名称 uint voteCount; // 累计得票数 }实际开发中我遇到过的一个坑是:委托循环。比如A委托给B,B又委托给A,这会导致无限循环消耗完所有gas。解决方法是在委托函数中加入循环检测:
while(voters[to].delegate != address(0)) { to = voters[to].delegate; require(to != msg.sender, "Found delegation loop!"); }2. 合约初始化与权限控制
合约部署时需要明确投票发起人(chairperson),通常这是合约的部署者地址。我建议采用以下初始化模式:
address public chairperson; constructor() { chairperson = msg.sender; voters[chairperson].weight = 1; // 发起人自动获得投票权 }提案的动态添加是个实用功能。在早期版本中,提案只能在构造函数中初始化,这很不灵活。改进后的方案:
function addProposals(string[] memory proposalNames) public { require(msg.sender == chairperson, "Only chairperson can add proposals"); for(uint i = 0; i < proposalNames.length; i++) { proposals.push(Proposal({ name: proposalNames[i], voteCount: 0 })); } }注意这里使用了memory关键字,因为字符串数组是临时参数。我曾犯过的错误是漏写这个修饰符,导致编译失败。
3. 投票权分配机制
投票权分配是最容易出问题的环节。必须确保:
- 只有主席能分配投票权
- 每个地址只能被分配一次
- 已经投票的地址不能再分配
function giveRightToVote(address voter) public { require(msg.sender == chairperson, "Not chairperson"); require(!voters[voter].voted, "Already voted"); require(voters[voter].weight == 0, "Already granted"); voters[voter].weight = 1; }在测试网上部署时,我发现一个常见错误:重复授权。比如这段有漏洞的代码:
// 错误示例:缺少weight检查 voters[voter].weight = 1;这会导致同一个地址可以被多次授权,严重破坏投票公平性。
4. 委托投票的实现细节
委托是本文最复杂的部分。核心逻辑是:
- 委托人不能委托给自己
- 要处理多级委托(A→B→C)
- 如果代理人已投票,票数直接加到其投票的提案
function delegate(address to) public { Voter storage sender = voters[msg.sender]; require(!sender.voted, "You already voted"); require(to != msg.sender, "Self-delegation disallowed"); // 寻找最终代理人 while(voters[to].delegate != address(0)) { to = voters[to].delegate; require(to != msg.sender, "Delegation loop detected"); } sender.voted = true; sender.delegate = to; Voter storage delegate_ = voters[to]; if(delegate_.voted) { proposals[delegate_.vote].voteCount += sender.weight; } else { delegate_.weight += sender.weight; } }实际项目中,我建议添加委托深度限制。虽然示例代码可以处理任意级委托,但过长的委托链可能耗尽gas。可以添加类似这样的检查:
uint delegationDepth; while(...) { delegationDepth++; require(delegationDepth < 10, "Delegation too deep"); }5. 投票与结果统计
直接投票的逻辑相对简单,但有几个关键点需要注意:
function vote(uint proposalIndex) public { Voter storage sender = voters[msg.sender]; require(!sender.voted, "Already voted"); require(proposalIndex < proposals.length, "Invalid proposal"); sender.voted = true; sender.vote = proposalIndex; proposals[proposalIndex].voteCount += sender.weight; }计票函数需要处理平局情况。原始示例只返回第一个得票最高的提案,这在实际中可能不够。改进方案:
function winningProposals() public view returns (uint[] memory) { uint maxVotes = 0; uint winnerCount = 0; // 第一次遍历找出最高票数 for(uint i = 0; i < proposals.length; i++) { if(proposals[i].voteCount > maxVotes) { maxVotes = proposals[i].voteCount; } } // 第二次遍历收集所有得票最高的提案 for(uint i = 0; i < proposals.length; i++) { if(proposals[i].voteCount == maxVotes) { winnerCount++; } } uint[] memory winners = new uint[](winnerCount); uint index = 0; for(uint i = 0; i < proposals.length; i++) { if(proposals[i].voteCount == maxVotes) { winners[index++] = i; } } return winners; }6. 安全增强与Gas优化
在正式环境中,还需要考虑以下安全措施:
- 防止重入攻击:虽然本例不涉及资金转移,但养成良好的编码习惯很重要
- 事件日志:所有关键操作应触发事件
- 提案上限:防止提案数组无限增长
event VoteGranted(address indexed voter); event Voted(address indexed voter, uint proposal); event Delegated(address indexed from, address indexed to); // 在giveRightToVote函数末尾添加: emit VoteGranted(voter); // 在vote函数末尾添加: emit Voted(msg.sender, proposalIndex); // 在delegate函数末尾添加: emit Delegated(msg.sender, to);Gas优化方面,可以:
- 使用
uint8代替uint256存储小数字 - 合并多个bool标记到一个uint中使用位运算
- 避免循环中的复杂计算
7. 完整合约部署与测试
建议使用Remix IDE进行初步测试,步骤如下:
- 编译合约(选择Solidity 0.8+版本)
- 在JavaScript VM环境中部署
- 测试流程:
// 添加提案 contract.addProposals(["Proposal A", "Proposal B"]) // 分配投票权 contract.giveRightToVote(account2) // 执行投票 contract.vote(0, {from: account2}) // 查询结果 contract.winningProposal()
在测试网(如Goerli)部署时,记得:
- 准备测试ETH支付gas费
- 使用MetaMask连接Remix
- 部署后保存合约地址和ABI
8. 可扩展性改进方向
这个基础合约还可以进一步扩展:
代币加权投票:用ERC20代币余额决定投票权重
IERC20 public votingToken; function vote(uint proposalIndex) public { uint balance = votingToken.balanceOf(msg.sender); // ...其余逻辑 proposals[proposalIndex].voteCount += balance; }投票时间段限制:
uint public votingStart; uint public votingEnd; modifier onlyDuringVotingPeriod { require(block.timestamp >= votingStart && block.timestamp <= votingEnd); _; }匿名投票:结合零知识证明技术
在最近的一个DAO项目中,我们就在此基础上增加了投票委托撤销功能。用户可以在投票截止前撤销委托,这需要维护更复杂的状态记录,但显著提高了灵活性。