Skip to content
On this page

L1-1: Token Voting(代币投票)

1. 问题

创建一个基于 ERC20 代币持有量的投票合约。代币持有者对单一提案投「赞成」或「反对」,投票权重 = 代币余额。核心难点:防止用户通过转移代币到其他地址来重复投票

2. 原因

传统投票系统只需检查「是否已投票」,但在代币投票中,用户可以:

  1. 用地址 A 的 100 代币投赞成
  2. 将 100 代币转到地址 B
  3. 用地址 B 的 100 代币再投一次

如果不追踪代币转移时的投票移除,同一批代币可以被无限次投票。

3. 方案

合约架构

  • DecentralizedResistanceToken.sol: ERC20 代币,在 _update() 中调用投票合约的 removeVotes(from)
  • TokenVoting.sol: 投票合约

核心机制

投票: vote(bool) → 记录投票方向 + 权重 → 累加 For/Against
移除: token合约调用 removeVotes(from) → 从计数中扣除 → 重置状态
结果: getResult() → votesFor > votesAgainst?

关键实现

  • removeVotes(address from) 仅代币合约可调用(onlyTokenContract modifier)
  • 追踪每个地址的投票方向 (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 中根据方向从 votesForvotesAgainst 扣除。

6.2

_update调用 removeVotes(from)(此时余额仍为旧值),调用 super._update()

6.3

使用 onlyTokenContract modifier 严格限制 removeVotes 的调用方。

7. 技术要点

要点说明
ERC20 _update override在代币转移钩子中集成业务逻辑
投票权重 = 代币余额快照式,非持续追踪
onlyTokenContract 访问控制msg.sender == address(token)
事件驱动架构VoteCasted / VotesRemoved 保证链上可审计
重入防护token 合约是已知可信方,但仍需注意回调风险

Built with AiAda