広告

第16話:【実践編①】オリジナルの投票システムを作ろう!

2025年7月31日

第16話:【実践編①】オリジナルの投票システムを作ろう!

概要

Solidity学習【実践編】の第一弾として、完全な投票システムをゼロから構築します。要件定義から始め、structとmappingでデータ構造を設計。Ownableを継承し、modifierで管理者機能を、requireで安全な投票ロジックを実装します。

eventで重要なアクションを記録するところまで、DApp開発の一連の流れを体験できる集大成的なレポートです。

目次
Solidity学習講座:最終版 目次(全20話)
Solidity学習講座:最終版 目次(全20話)

基礎編 第1話:未来のインターネットへようこそ!Solidityとスマートコントラクトの全体像 第2話:準備は1分!ブラウザだけで開発できる「Remix IDE」の基本操作 第3話:記念すべき初コント ...

続きを見る

はじめに

おめでとうございます!あなたは、Solidityの基本的な概念を学ぶ15話の長い旅路を終え、ついに実践の舞台へとたどり着きました。これまでのレッスンで、あなたはスマートコントラクトを構成する無数の「部品」を手に入れました。

  • structmappingによる、柔軟なデータ構造
  • functionmodifierによる、機能の実装とアクセス制御
  • eventによる、外部世界への情報伝達
  • if文やrequireによる、堅牢なロジック制御
  • Ownableの「継承」による、効率的なコードの再利用

しかし、これらの部品は、それ単体ではただのコードの断片です。本当の力は、これらを有機的に組み合わせ、一つの目的を持ったアプリケーションとして組み立てることで初めて発揮されます。

今回から始まる【実践編】では、まさにその「組み立て」のプロセスを体験していただきます。最初のプロジェクトは、ブロックチェーンの特性である透明性と耐改ざん性を活かすのに最適な**「オリジナルの投票システム」**です。

このレッスンを終える頃、あなたは単なる部品の知識を持つだけでなく、要件を定義し、データ構造を設計し、そして実際に機能するDAppを構築できる、本物の開発者としての一歩を踏み出しているはずです。

ステップ1:要件定義とデータ構造の設計

何を作るかが決まったら、次にやるべきは「どのような機能が必要か?」という要件定義です。今回の投票システムでは、以下の機能を目指します。

  1. 管理者: コントラクトをデプロイした人が管理者(オーナー)となる。
  2. 候補者登録: 管理者のみが、投票の対象となる「候補者(Proposal)」を登録できる。
  3. 有権者登録: 管理者のみが、投票する権利を持つ「有権者(Voter)」のアドレスを登録できる。
  4. 投票: 登録された有権者は、1人1回だけ、いずれかの候補者に投票できる。
  5. 結果確認: 誰でも、各候補者の現在の得票数を確認できる。
  6. 勝者判定: 誰でも、現時点で最も票を集めている候補者を知ることができる。

次に、この要件をSolidityのデータ構造に落とし込みます。

  • 候補者: 名前(string)と得票数(uint)を持つ。これはstructで表現するのが良さそうです。 struct Proposal { string name; uint voteCount; }
  • 有権者: 投票権があるか(isRegistered)、既に投票したか(hasVoted)、誰に投票したか(votedProposalId)の情報が必要。これもstructが最適です。 struct Voter { bool isRegistered; bool hasVoted; uint votedProposalId; }
  • 候補者リスト: 複数の候補者を管理する必要があるので、Proposal構造体の動的配列を使いましょう。 Proposal[] public proposals;
  • 有権者リスト: あるアドレスが有権者かどうかを瞬時に確認したい。これはmappingの得意分野です。 mapping(address => Voter) public voters;

ステップ2:コントラクトの骨格と状態変数の定義

設計ができたら、コードの骨格を組み上げます。管理者機能は、これまでのレッスンで作成したOwnableコントラクトを継承して、効率的に実装しましょう。

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

// 自作のOwnableコントラクトをインポート(同じ階層にあると仮定)
import "./Ownable.sol";

contract Voting is Ownable {
    // === データ構造の定義 ===
    struct Proposal {
        string name;
        uint voteCount;
    }

    struct Voter {
        bool isRegistered;
        bool hasVoted;
        uint votedProposalId;
    }

    // === 状態変数の定義 ===
    mapping(address => Voter) public voters;
    Proposal[] public proposals;

    // === イベントの定義 ===
    event VoterRegistered(address indexed voterAddress);
    event ProposalAdded(uint indexed proposalId);
    event Voted(address indexed voterAddress, uint indexed proposalId);
}

最初にstructmappingeventをまとめて定義しておくと、コード全体の見通しが良くなります。

ステップ3:管理者向け機能の実装

次に、onlyOwner修飾子を使って、管理者だけが実行できる機能を実装します。

Solidity
// 有権者のアドレスを登録する関数
function registerVoter(address _voterAddress) public onlyOwner {
    require(!voters[_voterAddress].isRegistered, "Voter is already registered.");
    voters[_voterAddress].isRegistered = true;
    emit VoterRegistered(_voterAddress);
}

// 候補者を追加する関数
function addProposal(string memory _name) public onlyOwner {
    // proposals配列の末尾に新しいProposalを追加
    proposals.push(Proposal({
        name: _name,
        voteCount: 0
    }));
    emit ProposalAdded(proposals.length - 1);
}

votersマッピングの_voterAddressをキーとして、そのisRegisteredtrueにすることで、有権者登録を実現しています。候補者の追加は、proposals配列にpushするだけです。簡単ですね。


ステップ4:投票機能の実装

いよいよ、このシステムの核心である投票機能です。不正が起きないよう、requireを使って慎重に条件をチェックします。

Solidity
// 投票する関数
function vote(uint _proposalId) public {
    // === Checks ===
    // 1. 登録された有権者か?
    require(voters[msg.sender].isRegistered, "You are not registered to vote.");
    // 2. まだ投票していないか?
    require(!voters[msg.sender].hasVoted, "You have already voted.");
    // 3. 存在する候補者IDか?
    require(_proposalId < proposals.length, "Invalid proposal ID.");

    // === Effects ===
    // 状態を変更
    voters[msg.sender].hasVoted = true;
    voters[msg.sender].votedProposalId = _proposalId;
    proposals[_proposalId].voteCount++;

    // === Interactions ===
    // イベントを放出
    emit Voted(msg.sender, _proposalId);
}

ここでも「Checks-Effects-Interactions」の考え方が活きています。まずrequireで条件をすべてチェックし、次にコントラクトの内部状態を変更し、最後にイベントを放出します。これにより、ロジックの安全性が高まります。

ステップ5:勝者判定機能の実装

最後に、誰でも投票結果を確認し、現在の勝者を判定できる関数を実装します。これは状態を変更しないのでview関数になります。

Solidity
// 勝者を判定する関数
function getWinner() public view returns (string memory winningProposalName) {
    uint maxVotes = 0;
    uint winningProposalId = 0;

    // forループで全候補者をチェック
    // view関数なので、ガス代は気にせずループできる(オフチェーン呼び出しの場合)
    for (uint i = 0; i < proposals.length; i++) {
        if (proposals[i].voteCount > maxVotes) {
            maxVotes = proposals[i].voteCount;
            winningProposalId = i;
        }
    }

    // 最も票が多かった候補者の名前を返す
    return proposals[winningProposalId].name;
}

forループを使っていますが、これはview関数なので、オフチェーン(ウェブサイトなど)から呼び出す限り、ユーザーはガス代を支払う必要はありません。

まとめ:あなたの手でDAppが生まれた!

お疲れ様でした!あなたは今、これまでに学んだ知識の断片を見事に組み合わせ、一つの意味のあるアプリケーション、投票システムをゼロから作り上げました。

ぜひこのコントラクトをRemixにデプロイし、遊んでみてください。

  1. addProposalで候補者を2〜3人登録する。
  2. registerVoterであなた自身のアドレスを有権者として登録する。
  3. voteで好きな候補者に投票してみる。
  4. proposalsvotersのパブリックなマッピング・配列を使い、状態が変化したことを確認する。
  5. getWinnerで勝者を確認する。

この経験は、あなたに大きな自信を与えてくれるはずです。

次回、**【実践編②】では、今回よりも少し複雑な、ETHのやり取りが発生する「クラウドファンディングコントラクト」**の構築に挑戦します。payable関数や、安全な送金ロジックなど、より実践的なスキルを身につけていきましょう。あなたのDApp開発者としての旅は、まだ始まったばかりです!

目次
Solidity学習講座:最終版 目次(全20話)
Solidity学習講座:最終版 目次(全20話)

基礎編 第1話:未来のインターネットへようこそ!Solidityとスマートコントラクトの全体像 第2話:準備は1分!ブラウザだけで開発できる「Remix IDE」の基本操作 第3話:記念すべき初コント ...

続きを見る

-Solidity