Appearance
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:
- Vote "For" with 100 tokens from address A
- Transfer the 100 tokens to address B
- 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'sremoveVotes(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 (onlyTokenContractmodifier)- 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
| Point | Description |
|---|---|
ERC20 _update override | Integrate business logic into the token transfer hook |
| Vote weight = token balance | Snapshot-based, not continuous tracking |
| onlyTokenContract access control | msg.sender == address(token) |
| Event-driven architecture | VoteCasted / VotesRemoved ensure on-chain auditability |
| Reentrancy protection | The token contract is a known trusted party, but callback risks still need attention |