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を累積
削除: トークンコントラクトがremoveVotes(from)を呼出 → カウントから控除 → 状態をリセット
結果: getResult() → votesFor > votesAgainst?
主要な実装
removeVotes(address from)はトークンコントラクトのみが呼び出し可能(onlyTokenContract修飾子)- 各アドレスの投票方向(
voteChoice)と重み(voteWeight)を追跡 - 投票削除時に方向に応じて正しいカウント項目から控除
4. 遭遇した落とし穴
4.1 removeVotesが投票方向を追跡していない
各アドレスが賛成か反対かを記録していないと、投票削除時にどのカウント項目から控除すればよいか判断できません。
4.2 トークンコントラクトの呼び出しタイミング
トークンコントラクトは転送前に古い残高を読み取る必要があります(この時点での残高はまだ転送前の数量です)。なぜなら、投票の重み = トークン残高だからです。
4.3 リエントランシーリスク
removeVotesがトークンコントラクトによって呼び出される際、投票コントラクトがさらにトークンコントラクトを呼び戻すと、リエントランシーが発生する可能性があります。onlyTokenContract修飾子を使用して呼び出し元を制限します。
5. 落とし穴の原因
5.1
投票方向の情報は、voteWeightが数値の重みのみを記録し、その重みが賛成・反対のどちらに寄与したかを保存していないために失われます。削除時には、どの累積値から減算するかを知る必要があります。
5.2
Solidityのsuper._update(from, to, amount)は最初に残高を更新し、その後にイベントを発行します。残高更新後に読み取ると、新しい残高(すでに減少済み)を取得してしまい、投票の重みが正しくなくなります。
6. 落とし穴の解決方法
6.1
voteChoiceマッピングを追加して投票方向を記録し、removeVotesで方向に基づいてvotesForまたはvotesAgainstから控除します。
6.2
_update内で、最初にremoveVotes(from)を呼び出し(この時点の残高はまだ旧値)、その後にsuper._update()を呼び出します。
6.3
onlyTokenContract修飾子を使用して、removeVotesの呼び出し元を厳密に制限します。
7. 技術的ポイント
| ポイント | 説明 |
|---|---|
ERC20 _update オーバーライド | トークン転送フックにビジネスロジックを統合 |
| 投票の重み = トークン残高 | スナップショット方式、継続的追跡ではない |
| onlyTokenContract アクセス制御 | msg.sender == address(token) |
| イベント駆動アーキテクチャ | VoteCasted / VotesRemovedによりオンチェーン監査が可能 |
| リエントランシー対策 | トークンコントラクトは既知の信頼できる当事者ですが、コールバックリスクには注意が必要 |