概要
Solidity学習【実践編】の第一弾として、完全な投票システムをゼロから構築します。要件定義から始め、structとmappingでデータ構造を設計。Ownableを継承し、modifierで管理者機能を、requireで安全な投票ロジックを実装します。
eventで重要なアクションを記録するところまで、DApp開発の一連の流れを体験できる集大成的なレポートです。
-

-
Solidity学習講座:最終版 目次(全20話)
基礎編 第1話:未来のインターネットへようこそ!Solidityとスマートコントラクトの全体像 第2話:準備は1分!ブラウザだけで開発できる「Remix IDE」の基本操作 第3話:記念すべき初コント ...
続きを見る
目次
はじめに
おめでとうございます!あなたは、Solidityの基本的な概念を学ぶ15話の長い旅路を終え、ついに実践の舞台へとたどり着きました。これまでのレッスンで、あなたはスマートコントラクトを構成する無数の「部品」を手に入れました。
structとmappingによる、柔軟なデータ構造functionとmodifierによる、機能の実装とアクセス制御eventによる、外部世界への情報伝達if文やrequireによる、堅牢なロジック制御Ownableの「継承」による、効率的なコードの再利用
しかし、これらの部品は、それ単体ではただのコードの断片です。本当の力は、これらを有機的に組み合わせ、一つの目的を持ったアプリケーションとして組み立てることで初めて発揮されます。
今回から始まる【実践編】では、まさにその「組み立て」のプロセスを体験していただきます。最初のプロジェクトは、ブロックチェーンの特性である透明性と耐改ざん性を活かすのに最適な**「オリジナルの投票システム」**です。
このレッスンを終える頃、あなたは単なる部品の知識を持つだけでなく、要件を定義し、データ構造を設計し、そして実際に機能するDAppを構築できる、本物の開発者としての一歩を踏み出しているはずです。
ステップ1:要件定義とデータ構造の設計
何を作るかが決まったら、次にやるべきは「どのような機能が必要か?」という要件定義です。今回の投票システムでは、以下の機能を目指します。
- 管理者: コントラクトをデプロイした人が管理者(オーナー)となる。
- 候補者登録: 管理者のみが、投票の対象となる「候補者(Proposal)」を登録できる。
- 有権者登録: 管理者のみが、投票する権利を持つ「有権者(Voter)」のアドレスを登録できる。
- 投票: 登録された有権者は、1人1回だけ、いずれかの候補者に投票できる。
- 結果確認: 誰でも、各候補者の現在の得票数を確認できる。
- 勝者判定: 誰でも、現時点で最も票を集めている候補者を知ることができる。
次に、この要件を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);
}
最初にstruct、mapping、eventをまとめて定義しておくと、コード全体の見通しが良くなります。
ステップ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をキーとして、そのisRegisteredをtrueにすることで、有権者登録を実現しています。候補者の追加は、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にデプロイし、遊んでみてください。
addProposalで候補者を2〜3人登録する。registerVoterであなた自身のアドレスを有権者として登録する。voteで好きな候補者に投票してみる。proposalsやvotersのパブリックなマッピング・配列を使い、状態が変化したことを確認する。getWinnerで勝者を確認する。
この経験は、あなたに大きな自信を与えてくれるはずです。
次回、**【実践編②】では、今回よりも少し複雑な、ETHのやり取りが発生する「クラウドファンディングコントラクト」**の構築に挑戦します。payable関数や、安全な送金ロジックなど、より実践的なスキルを身につけていきましょう。あなたのDApp開発者としての旅は、まだ始まったばかりです!
-

-
Solidity学習講座:最終版 目次(全20話)
基礎編 第1話:未来のインターネットへようこそ!Solidityとスマートコントラクトの全体像 第2話:準備は1分!ブラウザだけで開発できる「Remix IDE」の基本操作 第3話:記念すべき初コント ...
続きを見る

