Skip to content
On this page

L1-1: Token Voting

1. Problem

Create a voting contract based on ERC20 token holdings. Token holders vote "For" or "Against" a single proposal, with vote weight = token balance. Core challenge: preventing users from voting multiple times by transferring tokens to other addresses.

2. Why

Traditional voting systems only need to check "has already voted", but in token voting, users can:

  1. Vote "For" with 100 tokens from address A
  2. Transfer the 100 tokens to address B
  3. Vote again with the 100 tokens from address B

If vote removal during token transfers is not tracked, the same batch of tokens can be voted infinitely many times.

3. Solution

Contract Architecture

  • DecentralizedResistanceToken.sol: ERC20 token that calls the voting contract's removeVotes(from) in _update()
  • TokenVoting.sol: Voting contract

Core Mechanism

Vote: vote(bool) → records vote direction + weight → accumulates For/Against
Removal: token contract calls removeVotes(from) → deducts from counts → resets state
Result: getResult() → votesFor > votesAgainst?

Key Implementation

  • removeVotes(address from) callable only by the token contract (onlyTokenContract modifier)
  • Tracks each address's vote direction (voteChoice) and weight (voteWeight)
  • When removing votes, deducts from the correct count based on direction

4. Pitfalls Encountered

4.1 removeVotes does not track vote direction

Without recording whether each address voted For or Against, there is no way to know which count to deduct from when removing votes.

4.2 Token contract call timing

The token contract must read the old balance before the transfer (at this point the balance is still the pre-transfer amount), because vote weight = token balance.

4.3 Reentrancy risk

When removeVotes is called by the token contract, if the voting contract calls back into the token contract, a reentrancy loop could form. Use the onlyTokenContract modifier to restrict the caller.

5. Why the Pitfalls Happen

5.1

Vote direction information is lost because voteWeight only records the numeric weight, without storing whether that weight contributed to For or Against. When removing, you need to know which cumulative value to subtract from.

5.2

Solidity's super._update(from, to, amount) updates the balance first, then emits events. If you read the balance after the update, you get the new balance (already reduced), and the vote weight will be incorrect.

6. How to Resolve the Pitfalls

6.1

Add a voteChoice mapping to record vote direction, and in removeVotes deduct from votesFor or votesAgainst based on direction.

6.2

In _update, call removeVotes(from) first (balance is still the old value), then call super._update().

6.3

Use the onlyTokenContract modifier to strictly restrict the caller of removeVotes.

7. Technical Highlights

PointDescription
ERC20 _update overrideIntegrate business logic into the token transfer hook
Vote weight = token balanceSnapshot-based, not continuous tracking
onlyTokenContract access controlmsg.sender == address(token)
Event-driven architectureVoteCasted / VotesRemoved ensure on-chain auditability
Reentrancy protectionThe token contract is a known trusted party, but callback risks still need attention

Built with AiAda