【入門編】 DAOの委任(Delegation)機能における二重投票リスク – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!スマートコントラクトの開発やブロックチェーンのセキュリティに触れ始めたばかりの皆さん、日々の開発やお疲れ様です。「Web3の世界は自由で面白いけれど、お金や権利を扱うからこそ、少しのミスが大事件につながりそうで怖い…」そんな風に感じていませんか?

今回は、DAO(分散型自律組織)の根幹を支える「委任(Delegation)機能」に潜む、ちょっと油断すると大変なことになってしまう「二重投票リスク」について、身近な例えを交えながら一歩ずつ優しく紐解いていきたいと思います。

難しい専門用語が出てきても置いてけぼりにしませんので、ぜひ最後までリラックスして読んでいってくださいね!

—

1. 「DAOの委任」と「二重投票リスク」ってなに? 身近なたとえで考えてみよう

DAOでは、トークン(投票券のようなもの)を持っている人が、組織の方針を決める投票に参加できます。でも、「自分は忙しいから、信頼できるあの人に投票を丸投げ(委任)したい!」という時がありますよね。これが委任機能です。

ここで、身近な「マンションの管理組合の総会」にたとえてみましょう。

  • Aさんは、マンションの投票券を100枚持っています。
  • Aさんは忙しいので、住民代表のBさんに「私の100枚分の投票権利、あなたに預けるね(委任)」とお願いしました。
  • Bさんは、自分の分の投票券に加えて、Aさんから預かった100枚を使って、住民集会で自分の意見を大きくアピールできるようになりました。

さて、ここで問題(リスク)が発生します。
もし、ずる賢いAさんが、「あれ? Bさんに預けたけど、やっぱり今すぐお金に換えたいから、この100枚の投票券をCさんに売っちゃおう!」と、投票券を市場で売却してしまったらどうなるでしょうか?

さらに最悪なケースでは、「AさんがBさんに投票を委任した状態のまま、自分でもこっそり投票し、さらにBさんもその権利を使って投票してしまう(二重投票)」という不整合が起きてしまうのです。これでは、1票の価値が何重にも使われてしまい、DAOの民主主義がめちゃくちゃになってしまいますよね。

—

2. 攻撃者はここを狙う!二重投票のメカニズム

ブロックチェーンの世界では、すべてのデータがリアルタイムで誰から誰へ動くかが見えています。悪意ある攻撃者は、この仕組みの隙を突こうとします。

例えば、重要で大きな予算案の投票が行われる直前に、以下のようなずるい動きを企てるかもしれません。

1. 大量のトークンを持っているアカウントを用意する。
2. そのトークンを使って、自分の別のアカウントに投票権を「委任」する。
3. 委任された側のアカウントで、自分に有利なように不正な投票を大量に行う。
4. 投票が完了した(あるいは投票の最中に)、元のトークンを別のウォレットに売り払う、または移動させる。

リアルタイムの残高や状態だけで「今、何枚持っているか」を判定していると、こうした時間差のトリックにシステムが騙されてしまうのです。これが、DAOにおける二重投票リスクの正体です。

—

3. 救世主は「スナップショット(写真撮影)」!

この問題をピタッと解決する魔法の仕組みが、「スナップショットベースの投票集計」です。

またマンションの例えに戻りましょう。
管理組合の理事長が、「今年の重要方針の投票は、〇月〇日の午後3時時点の住民名簿に載っている人だけで行います!」とルールを決めました。

これならどうでしょう?

  • 〇月〇日の午後3時を過ぎたあとに、Aさんがこっそり投票券をCさんに売ったり、誰かに委任し直したりしても、「あの時(午後3時の写真)」の記録が変わるわけではありません。
  • だから、過去の一瞬を切り取った写真(スナップショット)を基準に集計すれば、売買と投票のタイミングがごちゃ混ぜになって不正が起きるのを完全に防ぐことができるんです!

スマートコントラクトの世界でも全く同じことをやります。特定のブロック番号(例:ブロック #12345678 時点)を指定し、「そのブロックが生成された瞬間に、誰が誰に投票権を委任していたか」の履歴だけを参照して票を集計するのです。

—

4. 【実践】安全なスナップショット委任・投票コントラクトの実装例

それでは、実際に安全なスナップショットベースの投票集計を行うスマートコントラクト(Solidity)のコードを見てみましょう。
初心者の方でも理解しやすいように、日本語でたっぷりとコメントを入れています!

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title スナップショットベースの委任投票コントラクト
 * @notice トークンの委任と売買が同時に起きても二重投票を防ぐ安全な実装例です
 */
contract SafeSnapshotVoting {
    
    // ユーザーごとのトークン残高の履歴を管理する構造体
    // どのブロックで何枚持っていたかを記録します(タイムスタンプ付きの通帳のようなもの)
    struct Checkpoint {
        uint32 fromBlock; // 記録されたブロック番号
        uint224 votes;    // その時点の投票権の数
    }

    // ユーザーのアドレス => チェックポイントの配列
    mapping(address => Checkpoint[]) public checkpoints;
    
    // 誰が誰に投票権を委任しているか(委任先のアドレス)
    mapping(address => address) public delegates;

    // 投票が行われたかどうかを記録するマップ (提案ID => 投票者 => 投票済みか)
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    /**
     * @notice 自分の投票権を別の人に「委任」する関数
     * @param delegatee 委任先(預かる人)のアドレス
     */
    function delegate(address delegatee) external {
        address currentDelegate = delegates[msg.sender];
        delegates[msg.sender] = delegatee;
        
        // 実際のアプリではここで委任変更に伴う投票権の移動処理(moveDelegates)を呼び出します
        // 今回はシンプルな概念説明のため省略しますが、内部でチェックポイントが新しく追加されます。
    }

    /**
     * @notice 指定した過去のブロック時点での投票権(残高+委任分)を取得する関数
     * @param account 調べたいユーザーのアドレス
     * @param blockNumber 過去のどのブロック時点を調べるか(スナップショット)
     */
    function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) {
        require(blockNumber < block.number, "未来のブロックを指定することはできません");

        Checkpoint[] memory accountCheckpoints = checkpoints[account];
        
        // 履歴が全くない場合は0を返す
        if (accountCheckpoints.length == 0) {
            return 0;
        }

        // 指定されたブロック番号時点のデータを二分探索などで安全に探し出す(ここでは簡略化)
        // リアルタイムの残高ではなく、「過去の写真」から正確な数値を引き出すのがポイント!
        // ... (ここに正確なチェックポイント検索ロジックが入ります)
        
        return accountCheckpoints[accountCheckpoints.length - 1].votes;
    }

    /**
     * @notice 提案に対して投票を行う関数
     * @param proposalId 提案のID
     * @param snapshotBlock 投票集計の基準となる「過去のブロック番号(スナップショット)」
     */
    function vote(uint256 proposalId, uint256 snapshotBlock) external {
        // ① 二重投票のチェック
        require(!hasVoted[proposalId][msg.sender], "あなたはすでにこの提案に投票しています!");
        
        // ② スナップショットブロックが過去のものであることを確認(未来や現在すぎると操作されるため)
        require(snapshotBlock < block.number, "スナップショットのブロックが無効です");

        // ③ 「過去の写真(スナップショット)」時点での正確な投票権を取得する
        // これにより、今さっきトークンを売却した人や、不正に集めた人の影響をシャットアウト!
        uint256 votingPower = getPastVotes(msg.sender, snapshotBlock);
        require(votingPower > 0, "投票権(パワー)がありません");

        // ④ 投票済みフラグを立てる
        hasVoted[proposalId][msg.sender] = true;

        // ⑤ 票の集計処理(例:賛成票に votingPower を加算する)
        // tallyVotes(proposalId, votingPower);
    }
}

コードのここがポイント!

  • snapshotBlock という引数に注目してください。投票する人が「どの瞬間のデータを基準にするか」を明示的に指定、またはシステム側で強制することで、現在の保有量に依存しない公平な集計を実現しています。
  • hasVoted マップによって、同じ人が同じ提案に対して二度投票できないよう、がっちりとガードを固めています。

—

безопасность (セキュリティ) の現場からひとこと

実際のインシデント現場や監査の現場では、「スナップショットを取るブロックの選定」が少しでもズレていると、攻撃者にその隙を突かれることがあります。例えば、提案が作成された瞬間のブロックを自動でスナップショットに指定する設計にするなど、「人間の手動オペレーションを極力減らし、スマートコントラクトが自動で安全なブロックを基準にする仕組み」を作ることが何よりも大切です。

初めてセキュリティやスマートコントラクトに触れるときは難しく感じるかもしれませんが、「過去の写真(スナップショット)を基準にする」という大原則さえ押さえておけば、悪意ある二重投票のリスクから大切なDAOコミュニティを守ることができます。

一歩ずつ、確実に安全なコードを書けるエンジニアを目指して一緒にがんばっていきましょう!質問や感想があれば、ぜひコメント欄やコミュニティで教えてくださいね。

コメント

タイトルとURLをコピーしました