Appearance
L1-1: Token Voting(代币投票)
1. 问题
创建一个基于 ERC20 代币持有量的投票合约。代币持有者对单一提案投「赞成」或「反对」,投票权重 = 代币余额。核心难点:防止用户通过转移代币到其他地址来重复投票。
2. 原因
传统投票系统只需检查「是否已投票」,但在代币投票中,用户可以:
- 用地址 A 的 100 代币投赞成
- 将 100 代币转到地址 B
- 用地址 B 的 100 代币再投一次
如果不追踪代币转移时的投票移除,同一批代币可以被无限次投票。
3. 方案
合约架构
DecentralizedResistanceToken.sol: ERC20 代币,在_update()中调用投票合约的removeVotes(from)TokenVoting.sol: 投票合约
核心机制
投票: vote(bool) → 记录投票方向 + 权重 → 累加 For/Against
移除: token合约调用 removeVotes(from) → 从计数中扣除 → 重置状态
结果: getResult() → votesFor > votesAgainst?
关键实现
removeVotes(address from)仅代币合约可调用(onlyTokenContractmodifier)- 追踪每个地址的投票方向 (
voteChoice) 和权重 (voteWeight) - 移除投票时根据方向从正确的计数字段扣除
4. 遇到的陷阱
4.1 removeVotes 未追踪投票方向
如果不记录每个地址投的是 For 还是 Against,移除投票时无法知道从哪个计数字段扣除。
4.2 代币合约调用时机
代币合约必须在转账之前读取旧余额(此时余额仍是转账前的数量),因为投票权重 = 代币余额。
4.3 重入风险
removeVotes 被代币合约调用时,如果投票合约又回调代币合约,可能形成重入。使用 onlyTokenContract modifier 限制调用方。
5. 陷阱的原因
5.1
投票方向信息丢失是因为 voteWeight 只记录了权重数字,没有存储该权重贡献给了 For 还是 Against。移除时需要知道从哪个累计值中减去。
5.2
Solidity 的 super._update(from, to, amount) 会先更新余额,再触发事件。如果在更新余额后才读取,读到的是新余额(已经减少),投票权重就不对了。
6. 如何解决陷阱
6.1
添加 voteChoice mapping 记录投票方向,removeVotes 中根据方向从 votesFor 或 votesAgainst 扣除。
6.2
在 _update 中先调用 removeVotes(from)(此时余额仍为旧值),再调用 super._update()。
6.3
使用 onlyTokenContract modifier 严格限制 removeVotes 的调用方。
7. 技术要点
| 要点 | 说明 |
|---|---|
ERC20 _update override | 在代币转移钩子中集成业务逻辑 |
| 投票权重 = 代币余额 | 快照式,非持续追踪 |
| onlyTokenContract 访问控制 | msg.sender == address(token) |
| 事件驱动架构 | VoteCasted / VotesRemoved 保证链上可审计 |
| 重入防护 | token 合约是已知可信方,但仍需注意回调风险 |